arclith-cli 0.10.0__tar.gz → 0.11.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 (30) hide show
  1. {arclith_cli-0.10.0 → arclith_cli-0.11.0}/PKG-INFO +2 -2
  2. {arclith_cli-0.10.0 → arclith_cli-0.11.0}/README.md +70 -12
  3. arclith_cli-0.11.0/arclith_cli/__init__.py +1 -0
  4. {arclith_cli-0.10.0 → arclith_cli-0.11.0}/arclith_cli/add_adapter.py +75 -1
  5. {arclith_cli-0.10.0 → arclith_cli-0.11.0}/arclith_cli/capabilities.py +155 -17
  6. {arclith_cli-0.10.0 → arclith_cli-0.11.0}/arclith_cli/core_scaffold.py +95 -2
  7. arclith_cli-0.11.0/arclith_cli/init_project.py +283 -0
  8. {arclith_cli-0.10.0 → arclith_cli-0.11.0}/arclith_cli/main.py +46 -2
  9. {arclith_cli-0.10.0 → arclith_cli-0.11.0}/arclith_cli/project_paths.py +8 -0
  10. {arclith_cli-0.10.0 → arclith_cli-0.11.0}/pyproject.toml +2 -2
  11. {arclith_cli-0.10.0 → arclith_cli-0.11.0}/tests/test_add_adapter.py +105 -8
  12. {arclith_cli-0.10.0 → arclith_cli-0.11.0}/tests/test_capabilities.py +38 -5
  13. arclith_cli-0.11.0/tests/test_core_scaffold.py +208 -0
  14. {arclith_cli-0.10.0 → arclith_cli-0.11.0}/tests/test_e2e_scaffold.py +101 -2
  15. {arclith_cli-0.10.0 → arclith_cli-0.11.0}/uv.lock +6 -2
  16. arclith_cli-0.10.0/arclith_cli/__init__.py +0 -1
  17. arclith_cli-0.10.0/tests/test_core_scaffold.py +0 -91
  18. {arclith_cli-0.10.0 → arclith_cli-0.11.0}/.gitignore +0 -0
  19. {arclith_cli-0.10.0 → arclith_cli-0.11.0}/LICENSE +0 -0
  20. {arclith_cli-0.10.0 → arclith_cli-0.11.0}/Makefile +0 -0
  21. {arclith_cli-0.10.0 → arclith_cli-0.11.0}/arclith_cli/adapter_templates.py +0 -0
  22. {arclith_cli-0.10.0 → arclith_cli-0.11.0}/arclith_cli/entity_scanner.py +0 -0
  23. {arclith_cli-0.10.0 → arclith_cli-0.11.0}/arclith_cli/export_config.py +0 -0
  24. {arclith_cli-0.10.0 → arclith_cli-0.11.0}/arclith_cli/rename.py +0 -0
  25. {arclith_cli-0.10.0 → arclith_cli-0.11.0}/arclith_cli/scaffold.py +0 -0
  26. {arclith_cli-0.10.0 → arclith_cli-0.11.0}/arclith_cli/updater.py +0 -0
  27. {arclith_cli-0.10.0 → arclith_cli-0.11.0}/tests/__init__.py +0 -0
  28. {arclith_cli-0.10.0 → arclith_cli-0.11.0}/tests/e2e_manual.sh +0 -0
  29. {arclith_cli-0.10.0 → arclith_cli-0.11.0}/tests/test_project_paths.py +0 -0
  30. {arclith_cli-0.10.0 → arclith_cli-0.11.0}/tests/test_rename.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arclith-cli
3
- Version: 0.10.0
3
+ Version: 0.11.0
4
4
  Summary: CLI scaffolding tool for arclith — hexagonal architecture framework
5
5
  Author-email: Killian KOPP <killiankopp@gmail.com>
6
6
  License: Apache License
@@ -186,7 +186,7 @@ License: Apache License
186
186
  License-File: LICENSE
187
187
  Keywords: arclith,cli,ddd,hexagonal-architecture,scaffold
188
188
  Requires-Python: >=3.13
189
- Requires-Dist: arclith>=0.13.0
189
+ Requires-Dist: arclith>=0.14.0
190
190
  Requires-Dist: httpx>=0.27.0
191
191
  Requires-Dist: rich>=13.0.0
192
192
  Requires-Dist: typer>=0.15.0
@@ -10,6 +10,25 @@ uv tool install "git+https://github.com/karned-rekipe/arclith.git#subdirectory=c
10
10
 
11
11
  ## Commandes
12
12
 
13
+ ### `init` — Initialiser un projet minimal
14
+
15
+ Crée un projet Arclith vide de métier, avec le layout canonique `src/<package>/...`, une
16
+ configuration minimale et un `main.py` prêt à recevoir les adapters.
17
+
18
+ ```bash
19
+ # Mode interactif
20
+ arclith-cli init
21
+
22
+ # Mode direct
23
+ arclith-cli init todo-list-service
24
+ arclith-cli init todo-list-service --dir ~/projects
25
+ ```
26
+
27
+ Cette commande ne crée aucune entité, aucun CRUD et aucun endpoint métier. Elle sert quand on veut
28
+ construire le projet étape par étape avec `add-entity`, `add-usecase`, puis `add-adapter`.
29
+
30
+ ---
31
+
13
32
  ### `new` — Créer un projet
14
33
 
15
34
  Scaffold un nouveau projet arclith depuis le template officiel `_sample`.
@@ -55,7 +74,8 @@ La commande ne génère aucun CRUD, aucun port repository, aucun adapter et aucu
55
74
 
56
75
  ### `add-usecase` — Ajouter un cas d'usage
57
76
 
58
- Crée uniquement le fichier minimal d'un cas d'usage dans `src/<package>/application/use_cases/`.
77
+ Crée le port inbound minimal dans `src/<package>/domain/ports/inbound/`, puis le fichier minimal du
78
+ cas d'usage dans `src/<package>/application/use_cases/`.
59
79
 
60
80
  ```bash
61
81
  cd my-recipe-service
@@ -66,12 +86,35 @@ arclith-cli add-usecase find-by-name
66
86
  Fichier généré :
67
87
 
68
88
  ```text
89
+ src/<package>/domain/ports/inbound/plan_shopping_list.py
69
90
  src/<package>/application/use_cases/plan_shopping_list.py
70
91
  ```
71
92
 
72
93
  Le nom peut être fourni en PascalCase, snake_case ou kebab-case. Le suffixe `UseCase` est normalisé : `PlanShoppingListUseCase` et `plan-shopping-list-use-case` génèrent tous les deux `PlanShoppingListUseCase`.
73
94
 
74
- Comme `add-entity`, cette commande ne câble pas FastAPI, FastMCP, LangGraph, un repository ou un service. Les adapters se branchent ensuite explicitement avec `add-adapter`.
95
+ Comme `add-entity`, cette commande ne câble pas FastAPI, FastMCP, LangGraph, un repository ou un service. Les adapters se branchent ensuite explicitement avec `add-adapter` et devraient dépendre du port inbound généré.
96
+
97
+ ---
98
+
99
+ ### `add-planner` — Ajouter un planner applicatif
100
+
101
+ Crée uniquement le fichier minimal d'un planner dans `src/<package>/application/planners/`.
102
+
103
+ ```bash
104
+ cd my-recipe-service
105
+ arclith-cli add-planner IngredientIntent
106
+ arclith-cli add-planner command-router
107
+ ```
108
+
109
+ Fichier généré :
110
+
111
+ ```text
112
+ src/<package>/application/planners/ingredient_intent.py
113
+ ```
114
+
115
+ Le planner est le composant applicatif qui transforme une demande naturelle en commande ou DTO
116
+ structuré. Il ne remplace pas LangGraph : LangGraph orchestre les nœuds, tandis que le planner porte
117
+ la traduction d'intention. Le fichier généré reste volontairement vide de logique métier.
75
118
 
76
119
  ---
77
120
 
@@ -92,6 +135,7 @@ arclith-cli add-adapter --adapter duckdb --all-entities --path data/ --no-activa
92
135
  arclith-cli add-adapter --adapter mariadb --entity Recipe --param database=my_recipe_service --param user=app --yes
93
136
  arclith-cli add-adapter --capability api --adapter fastapi --param port=8080 --yes
94
137
  arclith-cli add-adapter --capability mcp --adapter fastmcp --param port=8081 --yes
138
+ arclith-cli add-adapter --capability llm --adapter lmstudio --param model_name=qwen/qwen3.5-9b --yes
95
139
  arclith-cli add-adapter --capability agent --adapter langgraph --param graph_name=recipe_agent --yes
96
140
  arclith-cli add-adapter --capability observability --adapter langsmith
97
141
  arclith-cli add-adapter --capability observability --adapter opentelemetry --param service_name=my_recipe_service --yes
@@ -100,25 +144,28 @@ arclith-cli add-adapter --capability repository --adapter memory --entity Recipe
100
144
 
101
145
  **Étapes du wizard :**
102
146
 
103
- 1. **Type d'adapter** — selon la capacité : `memory` · `mongodb` · `duckdb` · `mariadb` · `fastapi` · `fastmcp` · `langgraph` · `langsmith` · `opentelemetry`
104
- 2. **Entité(s) cible(s)** — détectées automatiquement pour les adapters entity-scoped ; ignorées pour les transports globaux, `agent/langgraph` et les adapters d'observability
147
+ 1. **Type d'adapter** — selon la capacité : `memory` · `mongodb` · `duckdb` · `mariadb` · `fastapi` · `fastmcp` · `lmstudio` · `openai` · `anthropic` · `langgraph` · `langsmith` · `opentelemetry`
148
+ 2. **Entité(s) cible(s)** — détectées automatiquement pour les adapters entity-scoped ; ignorées pour les transports globaux, `llm/*`, `agent/langgraph` et les adapters d'observability
105
149
  3. **Paramètres** — questions spécifiques à l'adapter :
106
150
  - `mongodb` → `db_name`, `multitenant`
107
151
  - `duckdb` → `path`
108
152
  - `mariadb` → `host`, `port`, `database`, `user`, `driver`, `table_prefix`
109
153
  - `fastapi` → `host`, `port`, `reload`
110
154
  - `fastmcp` → `host`, `port`
155
+ - `lmstudio` → `model_name`, `base_url`, `api_key`
156
+ - `openai` → `model_name`, `base_url`, `OPENAI_API_KEY`
157
+ - `anthropic` → `model_name`, `ANTHROPIC_API_KEY`
111
158
  - `langgraph` → `graph_name`
112
159
  - `langsmith` → `tracing`, `project`, `endpoint`, `LANGSMITH_API_KEY`
113
160
  - `opentelemetry` → `service_name`, `endpoint`, `protocol`, `traces`, `metrics`, `instrument_fastapi`
114
161
  - `memory` → aucun paramètre
115
- 4. **Activation** — met à jour `config/adapters/adapters.yaml` pour les capacités à sélecteur (`repository: <adapter>` ou `observability: <adapter>`) ; `api/fastapi`, `mcp/fastmcp` et `agent/langgraph` sont exposés par leurs fichiers `config/adapters/inbound/*.yaml`
162
+ 4. **Activation** — met à jour `config/adapters/adapters.yaml` pour les capacités activables (`repository: <adapter>` ou `observability.enabled: [<adapter>, ...]`) ; `api/fastapi`, `mcp/fastmcp`, `llm/*` et `agent/langgraph` sont exposés par leurs fichiers de configuration scopés
116
163
  5. **Récapitulatif** — liste des fichiers créés ou remplacés avant confirmation
117
164
 
118
165
  | Option | Défaut | Description |
119
166
  |--------|--------|-------------|
120
- | `--capability` | `repository` | Capacité cible du catalogue standardisé (`repository`, `api`, `mcp`, `agent`, `observability`) |
121
- | `--adapter` / `-a` | interactif | Adapter du catalogue : `memory`, `mongodb`, `duckdb`, `mariadb`, `fastapi`, `fastmcp`, `langgraph`, `langsmith`, `opentelemetry` |
167
+ | `--capability` | `repository` | Capacité cible du catalogue standardisé (`repository`, `api`, `mcp`, `llm`, `agent`, `observability`) |
168
+ | `--adapter` / `-a` | interactif | Adapter du catalogue : `memory`, `mongodb`, `duckdb`, `mariadb`, `fastapi`, `fastmcp`, `lmstudio`, `openai`, `anthropic`, `langgraph`, `langsmith`, `opentelemetry` |
122
169
  | `--entity` / `-e` | auto si une seule entité | Entité cible, liste séparée par virgule acceptée |
123
170
  | `--all-entities` | `false` | Génère l'adapter pour toutes les entités détectées |
124
171
  | `--activate/--no-activate` | `--activate` | Met à jour `config/adapters/adapters.yaml` quand la capacité expose une clé d'activation |
@@ -144,16 +191,21 @@ src/<package>/infrastructure/containers/<entity>_container.py # RepositoryRegis
144
191
 
145
192
  ```bash
146
193
  uv add "arclith[langgraph]"
194
+ arclith-cli add-adapter --capability llm --adapter lmstudio --param model_name=qwen/qwen3.5-9b --yes
147
195
  arclith-cli add-adapter --capability agent --adapter langgraph
148
196
  arclith-cli add-adapter --capability observability --adapter langsmith
149
197
  uv run langgraph dev --no-browser --allow-blocking --port 2024
150
198
  ```
151
199
 
200
+ L'adapter `llm/lmstudio` génère `config/adapters/outbound/lm.yaml`, chargé dans
201
+ `AppConfig.adapters.lm`. Les adapters `llm/openai` et `llm/anthropic` génèrent aussi un mapping
202
+ `config/secrets.yaml` vers `OPENAI_API_KEY` ou `ANTHROPIC_API_KEY`.
203
+
152
204
  L'adapter `agent/langgraph` génère `langgraph.json`, `config/adapters/inbound/langgraph.yaml` et
153
205
  `src/<package>/adapters/inbound/langgraph/agent.py`. Le projet ne modifie ensuite que ce fichier pour
154
206
  son agent. Comme `fastapi` et `fastmcp`, LangGraph est configuré par son nom produit dans
155
207
  `AppConfig.langgraph`, sans `adapters.agent`. L'adapter `observability/langsmith` génère
156
- `config/adapters/outbound/langsmith.yaml`, met
208
+ `config/adapters/outbound/langsmith.yaml`, l'ajoute à `observability.enabled`, met
157
209
  à jour `.env` et ajoute `.env` au `.gitignore` si besoin. LangSmith Studio devient l'endroit standard
158
210
  pour tester les agents. Une `LANGSMITH_API_KEY` déjà présente est conservée si aucune nouvelle valeur
159
211
  n'est fournie.
@@ -166,8 +218,10 @@ arclith-cli add-adapter --capability observability --adapter opentelemetry --par
166
218
  ```
167
219
 
168
220
  L'adapter `observability/opentelemetry` génère `config/adapters/outbound/opentelemetry.yaml`, met à
169
- jour `.env`, active `observability: opentelemetry` et branche l'instrumentation FastAPI quand
170
- `Arclith.fastapi()` construit l'application.
221
+ jour `.env`, l'ajoute à `observability.enabled` et branche l'instrumentation FastAPI quand
222
+ `Arclith.fastapi()` construit l'application. Il peut être activé en même temps que LangSmith.
223
+ Le fichier `opentelemetry.yaml` ne porte pas de flag `enabled`: l'activation se fait uniquement dans
224
+ `observability.enabled`.
171
225
 
172
226
  Parcours complet avec entité, API, LangGraph, LangSmith et LM Studio:
173
227
  [`docs/agent-quickstart.md`](../docs/agent-quickstart.md).
@@ -236,11 +290,12 @@ config/
236
290
  soft_delete.yaml # soft_delete: { retention_days }
237
291
  secrets.yaml # secrets: { resolver, mappings, vault, yaml }
238
292
  adapters/
239
- adapters.yaml # adapters: { logger, repository } ← adapter actif
293
+ adapters.yaml # adapters: { logger, repository, observability.enabled }
240
294
  outbound/
241
295
  mongodb.yaml # adapters.mongodb: { db_name, multitenant }
242
296
  duckdb.yaml # adapters.duckdb: { path, multitenant }
243
297
  mariadb.yaml # adapters.mariadb: { host, port, database, user, ... }
298
+ lm.yaml # adapters.lm: { provider, model_name, api_key, base_url }
244
299
  langsmith.yaml # adapters.langsmith: { tracing, project, endpoint, ... }
245
300
  opentelemetry.yaml # adapters.opentelemetry: { endpoint, protocol, traces, metrics, ... }
246
301
  inbound/
@@ -258,7 +313,10 @@ Pour changer l'adapter actif sans passer par le wizard :
258
313
  ```yaml
259
314
  # config/adapters/adapters.yaml
260
315
  repository: duckdb # memory | mongodb | duckdb | mariadb
261
- observability: langsmith # none | langsmith | opentelemetry
316
+ observability:
317
+ enabled:
318
+ - langsmith
319
+ - opentelemetry
262
320
  ```
263
321
 
264
322
  Pour MariaDB, ne committez pas le mot de passe. Mappez `adapters.mariadb.password` ou `adapters.mariadb.url` via `config/secrets.yaml`, un resolver `env` ou Vault.
@@ -0,0 +1 @@
1
+ __version__ = "0.11.0"
@@ -5,6 +5,7 @@ from pathlib import Path
5
5
  from typing import Any
6
6
 
7
7
  import typer
8
+ import yaml
8
9
  from rich.console import Console
9
10
  from rich.panel import Panel
10
11
  from rich.prompt import Confirm, Prompt
@@ -20,6 +21,7 @@ from .capabilities import (
20
21
  AdapterSpec,
21
22
  CapabilitySpec,
22
23
  ParameterSpec,
24
+ SecretMappingSpec,
23
25
  capability_names,
24
26
  get_capability,
25
27
  )
@@ -384,7 +386,7 @@ def _show_recap(
384
386
  cfg_path = project_dir / "config" / "adapters" / "adapters.yaml"
385
387
  table.add_row(
386
388
  str(cfg_path.relative_to(project_dir)),
387
- f"[cyan]mis à jour ({capability.activation_config_key})[/cyan]",
389
+ f"[cyan]mis à jour ({_activation_config_label(capability)})[/cyan]",
388
390
  )
389
391
 
390
392
  console.print()
@@ -409,6 +411,10 @@ def _list_generated_files(
409
411
  gitignore = project_dir / ".gitignore"
410
412
  files.append((gitignore, "mis à jour" if gitignore.exists() else "créé"))
411
413
 
414
+ if adapter.has_secret_mappings():
415
+ secrets_file = project_dir / "config" / "secrets.yaml"
416
+ files.append((secrets_file, "mis à jour" if secrets_file.exists() else "créé"))
417
+
412
418
  template_vars = _file_template_vars(project_dir, paths, adapter, params={})
413
419
  for file_template in adapter.file_templates:
414
420
  path = project_dir / render(file_template.path, template_vars)
@@ -430,6 +436,12 @@ def _list_generated_files(
430
436
  return files
431
437
 
432
438
 
439
+ def _activation_config_label(capability: CapabilitySpec) -> str:
440
+ if capability.name == "observability":
441
+ return "observability.enabled"
442
+ return capability.activation_config_key or ""
443
+
444
+
433
445
  # ── Step 5 : generate ─────────────────────────────────────────────────────────
434
446
 
435
447
  def _generate(
@@ -459,6 +471,11 @@ def _generate(
459
471
  _ensure_env_is_ignored(project_dir)
460
472
  console.print(f"[green]✓[/green] {env_path.relative_to(project_dir)}")
461
473
 
474
+ if adapter.has_secret_mappings():
475
+ secrets_path = project_dir / "config" / "secrets.yaml"
476
+ _merge_secrets_file(secrets_path, adapter.secret_mappings)
477
+ console.print(f"[green]✓[/green] {secrets_path.relative_to(project_dir)}")
478
+
462
479
  for file_template in adapter.file_templates:
463
480
  generated_path = project_dir / render(file_template.path, params)
464
481
  generated_path.parent.mkdir(parents=True, exist_ok=True)
@@ -538,6 +555,10 @@ def _file_template_vars(
538
555
  def _update_active_capability(project_dir: Path, capability: CapabilitySpec, adapter: AdapterSpec) -> None:
539
556
  if capability.activation_config_key is None:
540
557
  return
558
+ if capability.name == "observability":
559
+ _enable_observability_adapter(project_dir, adapter)
560
+ return
561
+
541
562
  cfg = project_dir / "config" / "adapters" / "adapters.yaml"
542
563
  key = capability.activation_config_key
543
564
  escaped_key = re.escape(key)
@@ -554,6 +575,29 @@ def _update_active_capability(project_dir: Path, capability: CapabilitySpec, ada
554
575
  console.print(f"[cyan]↺[/cyan] config/adapters/adapters.yaml → {key}: {adapter.name}")
555
576
 
556
577
 
578
+ def _enable_observability_adapter(project_dir: Path, adapter: AdapterSpec) -> None:
579
+ cfg = project_dir / "config" / "adapters" / "adapters.yaml"
580
+ data = _read_yaml_mapping(cfg)
581
+ existing = data.get("observability")
582
+ if isinstance(existing, dict) and isinstance(existing.get("enabled"), list):
583
+ enabled = []
584
+ for name in existing["enabled"]:
585
+ if isinstance(name, str) and name not in enabled:
586
+ enabled.append(name)
587
+ else:
588
+ enabled = []
589
+
590
+ if adapter.name not in enabled:
591
+ enabled.append(adapter.name)
592
+
593
+ data["observability"] = {"enabled": enabled}
594
+ cfg.parent.mkdir(parents=True, exist_ok=True)
595
+ cfg.write_text(yaml.safe_dump(data, sort_keys=False, allow_unicode=True), encoding="utf-8")
596
+ console.print(
597
+ f"[cyan]↺[/cyan] config/adapters/adapters.yaml → observability.enabled += {adapter.name}"
598
+ )
599
+
600
+
557
601
  def _parse_env_template(rendered: str) -> dict[str, str]:
558
602
  values: dict[str, str] = {}
559
603
  for line in rendered.splitlines():
@@ -595,6 +639,36 @@ def _merge_env_file(env_path: Path, updates: dict[str, str]) -> None:
595
639
  env_path.write_text("\n".join(merged_lines).rstrip("\n") + "\n", encoding="utf-8")
596
640
 
597
641
 
642
+ def _merge_secrets_file(secrets_path: Path, mappings: tuple[SecretMappingSpec, ...]) -> None:
643
+ secrets_path.parent.mkdir(parents=True, exist_ok=True)
644
+ data = _read_yaml_mapping(secrets_path)
645
+ resolver = data.get("resolver")
646
+ if not isinstance(resolver, str) or not resolver.strip():
647
+ data["resolver"] = "env"
648
+
649
+ existing_mappings = data.get("mappings")
650
+ if not isinstance(existing_mappings, dict):
651
+ existing_mappings = {}
652
+
653
+ merged_mappings = dict(existing_mappings)
654
+ for mapping in mappings:
655
+ merged_mappings[mapping.field_path] = mapping.secret_key
656
+ data["mappings"] = merged_mappings
657
+
658
+ rendered = yaml.safe_dump(data, sort_keys=False, allow_unicode=True)
659
+ secrets_path.write_text(rendered, encoding="utf-8")
660
+
661
+
662
+ def _read_yaml_mapping(path: Path) -> dict[str, Any]:
663
+ if not path.exists():
664
+ return {}
665
+
666
+ loaded = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
667
+ if isinstance(loaded, dict):
668
+ return dict(loaded)
669
+ return {}
670
+
671
+
598
672
  def _ensure_env_is_ignored(project_dir: Path) -> None:
599
673
  gitignore = project_dir / ".gitignore"
600
674
  if gitignore.exists():
@@ -38,6 +38,18 @@ class FileTemplateSpec:
38
38
  }
39
39
 
40
40
 
41
+ @dataclass(frozen=True)
42
+ class SecretMappingSpec:
43
+ field_path: str
44
+ secret_key: str
45
+
46
+ def to_dict(self) -> dict[str, str]:
47
+ return {
48
+ "field_path": self.field_path,
49
+ "secret_key": self.secret_key,
50
+ }
51
+
52
+
41
53
  @dataclass(frozen=True)
42
54
  class AdapterSpec:
43
55
  name: str
@@ -49,6 +61,7 @@ class AdapterSpec:
49
61
  env_path: str | None = None
50
62
  env_template: str = ""
51
63
  file_templates: tuple[FileTemplateSpec, ...] = ()
64
+ secret_mappings: tuple[SecretMappingSpec, ...] = ()
52
65
  parameters: tuple[ParameterSpec, ...] = ()
53
66
  entity_scoped: bool = True
54
67
 
@@ -61,6 +74,9 @@ class AdapterSpec:
61
74
  def has_file_templates(self) -> bool:
62
75
  return bool(self.file_templates)
63
76
 
77
+ def has_secret_mappings(self) -> bool:
78
+ return bool(self.secret_mappings)
79
+
64
80
  def to_dict(self) -> dict[str, Any]:
65
81
  return {
66
82
  "name": self.name,
@@ -70,6 +86,7 @@ class AdapterSpec:
70
86
  "config_path": self.config_path,
71
87
  "env_path": self.env_path,
72
88
  "file_templates": [file_template.to_dict() for file_template in self.file_templates],
89
+ "secret_mappings": [secret_mapping.to_dict() for secret_mapping in self.secret_mappings],
73
90
  "parameters": [parameter.to_dict() for parameter in self.parameters],
74
91
  "entity_scoped": self.entity_scoped,
75
92
  }
@@ -106,14 +123,14 @@ class CapabilitySpec:
106
123
  REPOSITORY_CAPABILITY = CapabilitySpec(
107
124
  name="repository",
108
125
  layer="outbound",
109
- description="Persistance des entites metier derriere un port repository.",
126
+ description="Persistance des entités métier derrière un port repository.",
110
127
  activation_config_key="repository",
111
128
  adapters=(
112
129
  AdapterSpec(
113
130
  name="memory",
114
131
  capability="repository",
115
132
  layer="outbound",
116
- description="Stockage volatile en memoire pour dev, tests et smoke locaux.",
133
+ description="Stockage volatile en mémoire pour dev, tests et smoke locaux.",
117
134
  ),
118
135
  AdapterSpec(
119
136
  name="mongodb",
@@ -144,7 +161,7 @@ db_name: {db_name} # uri -> secrets.yaml ou Vault (fallback single-tenant)
144
161
  name="duckdb",
145
162
  capability="repository",
146
163
  layer="outbound",
147
- description="Repository fichier local pour SQL analytique et demos sans serveur.",
164
+ description="Repository fichier local pour SQL analytique et démos sans serveur.",
148
165
  config_path="config/adapters/outbound/duckdb.yaml",
149
166
  config_template="""\
150
167
  multitenant: false
@@ -227,7 +244,7 @@ API_CAPABILITY = CapabilitySpec(
227
244
  name="fastapi",
228
245
  capability="api",
229
246
  layer="inbound",
230
- description="Application FastAPI configuree par Arclith.fastapi().",
247
+ description="Application FastAPI configurée par Arclith.fastapi().",
231
248
  config_path="config/adapters/inbound/fastapi.yaml",
232
249
  config_template="""\
233
250
  host: {host}
@@ -269,7 +286,7 @@ MCP_CAPABILITY = CapabilitySpec(
269
286
  name="fastmcp",
270
287
  capability="mcp",
271
288
  layer="inbound",
272
- description="Serveur FastMCP configure par Arclith.fastmcp() et les runners MCP.",
289
+ description="Serveur FastMCP configuré par Arclith.fastmcp() et les runners MCP.",
273
290
  config_path="config/adapters/inbound/fastmcp.yaml",
274
291
  config_template="""\
275
292
  host: {host}
@@ -294,17 +311,144 @@ port: {port}
294
311
  ),
295
312
  )
296
313
 
314
+ LLM_CAPABILITY = CapabilitySpec(
315
+ name="llm",
316
+ layer="outbound",
317
+ description="Configuration LLM pour planners et agents via une factory Arclith.",
318
+ activation_config_key=None,
319
+ adapters=(
320
+ AdapterSpec(
321
+ name="lmstudio",
322
+ capability="llm",
323
+ layer="outbound",
324
+ description="LLM local LM Studio exposé par l'API OpenAI-compatible.",
325
+ config_path="config/adapters/outbound/lm.yaml",
326
+ config_template="""\
327
+ provider: openai
328
+ model_name: "{model_name}"
329
+ api_key: "{api_key}"
330
+ base_url: "{base_url}"
331
+ """,
332
+ parameters=(
333
+ ParameterSpec(
334
+ name="model_name",
335
+ kind="string",
336
+ prompt="Model ID LM Studio",
337
+ default="remplacer-par-le-model-id-lm-studio",
338
+ ),
339
+ ParameterSpec(
340
+ name="base_url",
341
+ kind="string",
342
+ prompt="Endpoint OpenAI-compatible LM Studio",
343
+ default="http://127.0.0.1:1234/v1",
344
+ ),
345
+ ParameterSpec(
346
+ name="api_key",
347
+ kind="string",
348
+ prompt="API key LM Studio",
349
+ default="lm-studio",
350
+ secret=True,
351
+ ),
352
+ ),
353
+ entity_scoped=False,
354
+ ),
355
+ AdapterSpec(
356
+ name="openai",
357
+ capability="llm",
358
+ layer="outbound",
359
+ description="Modèle OpenAI via protocole OpenAI-compatible.",
360
+ config_path="config/adapters/outbound/lm.yaml",
361
+ config_template="""\
362
+ provider: openai
363
+ model_name: "{model_name}"
364
+ api_key: ""
365
+ base_url: "{base_url}"
366
+ """,
367
+ env_path=".env",
368
+ env_template="""\
369
+ OPENAI_API_KEY={api_key}
370
+ """,
371
+ secret_mappings=(
372
+ SecretMappingSpec(
373
+ field_path="adapters.lm.api_key",
374
+ secret_key="OPENAI_API_KEY",
375
+ ),
376
+ ),
377
+ parameters=(
378
+ ParameterSpec(
379
+ name="model_name",
380
+ kind="string",
381
+ prompt="Modèle OpenAI",
382
+ default="gpt-4o-mini",
383
+ ),
384
+ ParameterSpec(
385
+ name="base_url",
386
+ kind="string",
387
+ prompt="Endpoint OpenAI-compatible",
388
+ default="https://api.openai.com/v1",
389
+ ),
390
+ ParameterSpec(
391
+ name="api_key",
392
+ kind="string",
393
+ prompt="OPENAI_API_KEY",
394
+ default="",
395
+ secret=True,
396
+ ),
397
+ ),
398
+ entity_scoped=False,
399
+ ),
400
+ AdapterSpec(
401
+ name="anthropic",
402
+ capability="llm",
403
+ layer="outbound",
404
+ description="Modèle Anthropic pour planners et agents.",
405
+ config_path="config/adapters/outbound/lm.yaml",
406
+ config_template="""\
407
+ provider: anthropic
408
+ model_name: "{model_name}"
409
+ api_key: ""
410
+ """,
411
+ env_path=".env",
412
+ env_template="""\
413
+ ANTHROPIC_API_KEY={api_key}
414
+ """,
415
+ secret_mappings=(
416
+ SecretMappingSpec(
417
+ field_path="adapters.lm.api_key",
418
+ secret_key="ANTHROPIC_API_KEY",
419
+ ),
420
+ ),
421
+ parameters=(
422
+ ParameterSpec(
423
+ name="model_name",
424
+ kind="string",
425
+ prompt="Modèle Anthropic",
426
+ default="claude-sonnet-4-5",
427
+ ),
428
+ ParameterSpec(
429
+ name="api_key",
430
+ kind="string",
431
+ prompt="ANTHROPIC_API_KEY",
432
+ default="",
433
+ secret=True,
434
+ ),
435
+ ),
436
+ entity_scoped=False,
437
+ ),
438
+ ),
439
+ )
440
+
297
441
  AGENT_CAPABILITY = CapabilitySpec(
298
442
  name="agent",
299
443
  layer="inbound",
300
- description="Adapter agent qui expose les cas d'usage metier via un runtime IA.",
444
+ description="Adapter agent qui expose les cas d'usage métier via un runtime IA.",
301
445
  activation_config_key=None,
302
446
  adapters=(
303
447
  AdapterSpec(
304
448
  name="langgraph",
305
449
  capability="agent",
306
450
  layer="inbound",
307
- description="Entrypoint LangGraph Studio base sur la tuyauterie Arclith.",
451
+ description="Entrypoint LangGraph Studio basé sur la tuyauterie Arclith.",
308
452
  config_path="config/adapters/inbound/langgraph.yaml",
309
453
  config_template="""\
310
454
  name: "{graph_name}"
@@ -375,7 +519,7 @@ agent = arclith.langgraph(AgentState, register_agent, name="{graph_name}")
375
519
  OBSERVABILITY_CAPABILITY = CapabilitySpec(
376
520
  name="observability",
377
521
  layer="outbound",
378
- description="Observabilite et boucle de test agent via LangSmith Studio.",
522
+ description="Observabilité activable en parallèle via LangSmith et OpenTelemetry.",
379
523
  activation_config_key="observability",
380
524
  adapters=(
381
525
  AdapterSpec(
@@ -435,7 +579,6 @@ LANGSMITH_API_KEY={api_key}
435
579
  description="Export OTLP traces/metrics et instrumentation FastAPI.",
436
580
  config_path="config/adapters/outbound/opentelemetry.yaml",
437
581
  config_template="""\
438
- enabled: {enabled}
439
582
  service_name: "{service_name}"
440
583
  endpoint: "{endpoint}"
441
584
  protocol: "{protocol}"
@@ -453,12 +596,6 @@ OTEL_EXPORTER_OTLP_PROTOCOL={protocol}
453
596
  OTEL_EXPORTER_OTLP_HEADERS={headers}
454
597
  """,
455
598
  parameters=(
456
- ParameterSpec(
457
- name="enabled",
458
- kind="boolean",
459
- prompt="Activer OpenTelemetry",
460
- default=True,
461
- ),
462
599
  ParameterSpec(
463
600
  name="service_name",
464
601
  kind="string",
@@ -486,7 +623,7 @@ OTEL_EXPORTER_OTLP_HEADERS={headers}
486
623
  ParameterSpec(
487
624
  name="metrics",
488
625
  kind="boolean",
489
- prompt="Exporter les metriques",
626
+ prompt="Exporter les métriques",
490
627
  default=False,
491
628
  ),
492
629
  ParameterSpec(
@@ -498,7 +635,7 @@ OTEL_EXPORTER_OTLP_HEADERS={headers}
498
635
  ParameterSpec(
499
636
  name="metrics_export_interval_millis",
500
637
  kind="string",
501
- prompt="Intervalle export metriques en ms",
638
+ prompt="Intervalle export métriques en ms",
502
639
  default="60000",
503
640
  ),
504
641
  ParameterSpec(
@@ -518,6 +655,7 @@ CAPABILITY_CATALOG = (
518
655
  REPOSITORY_CAPABILITY,
519
656
  API_CAPABILITY,
520
657
  MCP_CAPABILITY,
658
+ LLM_CAPABILITY,
521
659
  AGENT_CAPABILITY,
522
660
  OBSERVABILITY_CAPABILITY,
523
661
  )