arclith-cli 0.18.0__tar.gz → 0.19.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 (50) hide show
  1. {arclith_cli-0.18.0 → arclith_cli-0.19.0}/PKG-INFO +2 -2
  2. arclith_cli-0.19.0/arclith_cli/__init__.py +1 -0
  3. {arclith_cli-0.18.0 → arclith_cli-0.19.0}/arclith_cli/capabilities.py +7 -1
  4. {arclith_cli-0.18.0 → arclith_cli-0.19.0}/arclith_cli/catalogs/ai.py +89 -0
  5. {arclith_cli-0.18.0 → arclith_cli-0.19.0}/arclith_cli/catalogs/persistence.py +129 -0
  6. {arclith_cli-0.18.0 → arclith_cli-0.19.0}/arclith_cli/init_project.py +1 -1
  7. {arclith_cli-0.18.0 → arclith_cli-0.19.0}/arclith_cli/main.py +1 -1
  8. {arclith_cli-0.18.0 → arclith_cli-0.19.0}/pyproject.toml +2 -2
  9. {arclith_cli-0.18.0 → arclith_cli-0.19.0}/tests/e2e_manual.sh +1 -1
  10. {arclith_cli-0.18.0 → arclith_cli-0.19.0}/tests/test_add_adapter.py +7 -7
  11. {arclith_cli-0.18.0 → arclith_cli-0.19.0}/tests/test_core_scaffold.py +1 -1
  12. {arclith_cli-0.18.0 → arclith_cli-0.19.0}/tests/test_embedding_capability.py +125 -3
  13. arclith_cli-0.19.0/tests/test_vector_store_capability.py +166 -0
  14. {arclith_cli-0.18.0 → arclith_cli-0.19.0}/uv.lock +5 -3
  15. arclith_cli-0.18.0/arclith_cli/__init__.py +0 -1
  16. {arclith_cli-0.18.0 → arclith_cli-0.19.0}/.gitignore +0 -0
  17. {arclith_cli-0.18.0 → arclith_cli-0.19.0}/LICENSE +0 -0
  18. {arclith_cli-0.18.0 → arclith_cli-0.19.0}/Makefile +0 -0
  19. {arclith_cli-0.18.0 → arclith_cli-0.19.0}/README.md +0 -0
  20. {arclith_cli-0.18.0 → arclith_cli-0.19.0}/arclith_cli/adapter_config.py +0 -0
  21. {arclith_cli-0.18.0 → arclith_cli-0.19.0}/arclith_cli/adapter_generator.py +0 -0
  22. {arclith_cli-0.18.0 → arclith_cli-0.19.0}/arclith_cli/adapter_parameters.py +0 -0
  23. {arclith_cli-0.18.0 → arclith_cli-0.19.0}/arclith_cli/adapter_rendering.py +0 -0
  24. {arclith_cli-0.18.0 → arclith_cli-0.19.0}/arclith_cli/adapter_selection.py +0 -0
  25. {arclith_cli-0.18.0 → arclith_cli-0.19.0}/arclith_cli/adapter_templates.py +0 -0
  26. {arclith_cli-0.18.0 → arclith_cli-0.19.0}/arclith_cli/add_adapter.py +0 -0
  27. {arclith_cli-0.18.0 → arclith_cli-0.19.0}/arclith_cli/capability_models.py +0 -0
  28. {arclith_cli-0.18.0 → arclith_cli-0.19.0}/arclith_cli/catalogs/__init__.py +0 -0
  29. {arclith_cli-0.18.0 → arclith_cli-0.19.0}/arclith_cli/catalogs/core.py +0 -0
  30. {arclith_cli-0.18.0 → arclith_cli-0.19.0}/arclith_cli/catalogs/observability.py +0 -0
  31. {arclith_cli-0.18.0 → arclith_cli-0.19.0}/arclith_cli/catalogs/security.py +0 -0
  32. {arclith_cli-0.18.0 → arclith_cli-0.19.0}/arclith_cli/catalogs/transports.py +0 -0
  33. {arclith_cli-0.18.0 → arclith_cli-0.19.0}/arclith_cli/core_scaffold.py +0 -0
  34. {arclith_cli-0.18.0 → arclith_cli-0.19.0}/arclith_cli/entity_scanner.py +0 -0
  35. {arclith_cli-0.18.0 → arclith_cli-0.19.0}/arclith_cli/export_config.py +0 -0
  36. {arclith_cli-0.18.0 → arclith_cli-0.19.0}/arclith_cli/new_project.py +0 -0
  37. {arclith_cli-0.18.0 → arclith_cli-0.19.0}/arclith_cli/project_paths.py +0 -0
  38. {arclith_cli-0.18.0 → arclith_cli-0.19.0}/arclith_cli/recipe.py +0 -0
  39. {arclith_cli-0.18.0 → arclith_cli-0.19.0}/arclith_cli/recipe_cli.py +0 -0
  40. {arclith_cli-0.18.0 → arclith_cli-0.19.0}/arclith_cli/recipe_models.py +0 -0
  41. {arclith_cli-0.18.0 → arclith_cli-0.19.0}/arclith_cli/rename.py +0 -0
  42. {arclith_cli-0.18.0 → arclith_cli-0.19.0}/arclith_cli/runtime_templates.py +0 -0
  43. {arclith_cli-0.18.0 → arclith_cli-0.19.0}/arclith_cli/scaffold.py +0 -0
  44. {arclith_cli-0.18.0 → arclith_cli-0.19.0}/arclith_cli/updater.py +0 -0
  45. {arclith_cli-0.18.0 → arclith_cli-0.19.0}/tests/__init__.py +0 -0
  46. {arclith_cli-0.18.0 → arclith_cli-0.19.0}/tests/test_capabilities.py +0 -0
  47. {arclith_cli-0.18.0 → arclith_cli-0.19.0}/tests/test_e2e_scaffold.py +0 -0
  48. {arclith_cli-0.18.0 → arclith_cli-0.19.0}/tests/test_project_paths.py +0 -0
  49. {arclith_cli-0.18.0 → arclith_cli-0.19.0}/tests/test_recipe.py +0 -0
  50. {arclith_cli-0.18.0 → arclith_cli-0.19.0}/tests/test_rename.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: arclith-cli
3
- Version: 0.18.0
3
+ Version: 0.19.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.21.0
189
+ Requires-Dist: arclith>=0.22.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
@@ -0,0 +1 @@
1
+ __version__ = "0.19.0"
@@ -24,7 +24,11 @@ from arclith_cli.catalogs.core import (
24
24
  SECRETS_CAPABILITY,
25
25
  )
26
26
  from arclith_cli.catalogs.observability import OBSERVABILITY_CAPABILITY
27
- from arclith_cli.catalogs.persistence import REPOSITORY_CAPABILITY, STORAGE_CAPABILITY
27
+ from arclith_cli.catalogs.persistence import (
28
+ REPOSITORY_CAPABILITY,
29
+ STORAGE_CAPABILITY,
30
+ VECTOR_STORE_CAPABILITY,
31
+ )
28
32
  from arclith_cli.catalogs.security import (
29
33
  AUTH_CAPABILITY,
30
34
  LICENSE_CAPABILITY,
@@ -68,6 +72,7 @@ __all__ = [
68
72
  "STORAGE_CAPABILITY",
69
73
  "SecretMappingSpec",
70
74
  "TENANT_CAPABILITY",
75
+ "VECTOR_STORE_CAPABILITY",
71
76
  "capability_catalog_as_dict",
72
77
  "capability_names",
73
78
  "get_capability",
@@ -77,6 +82,7 @@ __all__ = [
77
82
  CAPABILITY_CATALOG = (
78
83
  REPOSITORY_CAPABILITY,
79
84
  STORAGE_CAPABILITY,
85
+ VECTOR_STORE_CAPABILITY,
80
86
  CACHE_CAPABILITY,
81
87
  LOGGER_CAPABILITY,
82
88
  SECRETS_CAPABILITY,
@@ -260,6 +260,95 @@ multitenant: {multitenant}
260
260
  dependency_extra="embedding",
261
261
  entity_scoped=False,
262
262
  ),
263
+ AdapterSpec(
264
+ name="openai",
265
+ capability="embedding",
266
+ layer="outbound",
267
+ description="Embeddings via l'API OpenAI officielle et un secret externe.",
268
+ config_path="config/adapters/outbound/embedding.yaml",
269
+ config_template="""\
270
+ adapter: openai
271
+ base_url: "{base_url}"
272
+ api_key: null
273
+ model_name: "{model_name}"
274
+ dimensions: {dimensions}
275
+ batch_size: {batch_size}
276
+ timeout: {timeout}
277
+ encoding_format: {encoding_format}
278
+ normalize: {normalize}
279
+ multitenant: {multitenant}
280
+ """,
281
+ env_path=".env",
282
+ env_template="""\
283
+ OPENAI_API_KEY={api_key}
284
+ """,
285
+ secret_mappings=(
286
+ SecretMappingSpec(
287
+ field_path="adapters.embedding.api_key",
288
+ secret_key="OPENAI_API_KEY",
289
+ ),
290
+ ),
291
+ parameters=(
292
+ ParameterSpec(
293
+ name="base_url",
294
+ kind="string",
295
+ prompt="Endpoint officiel OpenAI avec préfixe API",
296
+ default="https://api.openai.com/v1",
297
+ ),
298
+ ParameterSpec(
299
+ name="api_key",
300
+ kind="string",
301
+ prompt="OPENAI_API_KEY (laisser vide pour un secret externe)",
302
+ default="",
303
+ secret=True,
304
+ ),
305
+ ParameterSpec(
306
+ name="model_name",
307
+ kind="string",
308
+ prompt="ID du modèle d'embedding OpenAI accessible au projet",
309
+ default="remplacer-par-model-id-openai-embedding",
310
+ ),
311
+ ParameterSpec(
312
+ name="dimensions",
313
+ kind="string",
314
+ prompt="Dimension optionnelle des vecteurs (null pour le défaut modèle)",
315
+ default="null",
316
+ ),
317
+ ParameterSpec(
318
+ name="batch_size",
319
+ kind="string",
320
+ prompt="Taille maximale des sous-batches",
321
+ default="64",
322
+ ),
323
+ ParameterSpec(
324
+ name="timeout",
325
+ kind="string",
326
+ prompt="Timeout HTTP en secondes",
327
+ default="30.0",
328
+ ),
329
+ ParameterSpec(
330
+ name="encoding_format",
331
+ kind="string",
332
+ prompt="Format des vecteurs retournés",
333
+ default="float",
334
+ choices=("float",),
335
+ ),
336
+ ParameterSpec(
337
+ name="normalize",
338
+ kind="boolean",
339
+ prompt="Normaliser les vecteurs en norme L2",
340
+ default=False,
341
+ ),
342
+ ParameterSpec(
343
+ name="multitenant",
344
+ kind="boolean",
345
+ prompt="Réserver la résolution de contexte par tenant",
346
+ default=False,
347
+ ),
348
+ ),
349
+ dependency_extra="embedding",
350
+ entity_scoped=False,
351
+ ),
263
352
  ),
264
353
  )
265
354
 
@@ -444,3 +444,132 @@ multitenant: {multitenant}
444
444
  ),
445
445
  ),
446
446
  )
447
+
448
+ VECTOR_STORE_CAPABILITY = CapabilitySpec(
449
+ name="vector-store",
450
+ layer="outbound",
451
+ description="Indexation et recherche dense derrière un port provider-neutral.",
452
+ activation_config_key=None,
453
+ adapters=(
454
+ AdapterSpec(
455
+ name="memory",
456
+ capability="vector-store",
457
+ layer="outbound",
458
+ description="Recherche exacte déterministe sans dépendance pour tests et POC.",
459
+ config_path="config/adapters/outbound/vector_store.yaml",
460
+ config_template="""\
461
+ adapter: memory
462
+ collection_name: "{collection_name}"
463
+ vector_size: {vector_size}
464
+ distance: {distance}
465
+ multitenant: {multitenant}
466
+ """,
467
+ parameters=(
468
+ ParameterSpec(
469
+ name="collection_name",
470
+ kind="string",
471
+ prompt="Nom logique de la collection",
472
+ default="default",
473
+ ),
474
+ ParameterSpec(
475
+ name="vector_size",
476
+ kind="string",
477
+ prompt="Dimension obligatoire des vecteurs",
478
+ default="1536",
479
+ ),
480
+ ParameterSpec(
481
+ name="distance",
482
+ kind="string",
483
+ prompt="Métrique de similarité",
484
+ default="cosine",
485
+ choices=("cosine", "dot", "euclid"),
486
+ ),
487
+ ParameterSpec(
488
+ name="multitenant",
489
+ kind="boolean",
490
+ prompt="Réserver la résolution de contexte par tenant",
491
+ default=False,
492
+ ),
493
+ ),
494
+ entity_scoped=False,
495
+ ),
496
+ AdapterSpec(
497
+ name="qdrant",
498
+ capability="vector-store",
499
+ layer="outbound",
500
+ description="Index vectoriel dense Qdrant via le client Python async officiel.",
501
+ config_path="config/adapters/outbound/vector_store.yaml",
502
+ config_template="""\
503
+ adapter: qdrant
504
+ url: "{url}"
505
+ api_key: null
506
+ collection_name: "{collection_name}"
507
+ vector_size: {vector_size}
508
+ distance: {distance}
509
+ prefer_grpc: {prefer_grpc}
510
+ timeout: {timeout}
511
+ create_collection: {create_collection}
512
+ multitenant: {multitenant}
513
+ """,
514
+ secret_mappings=(
515
+ SecretMappingSpec(
516
+ field_path="adapters.vector_store.api_key",
517
+ secret_key="QDRANT_API_KEY",
518
+ ),
519
+ ),
520
+ parameters=(
521
+ ParameterSpec(
522
+ name="url",
523
+ kind="string",
524
+ prompt="Endpoint HTTP Qdrant sans credentials",
525
+ default="http://localhost:6333",
526
+ ),
527
+ ParameterSpec(
528
+ name="collection_name",
529
+ kind="string",
530
+ prompt="Nom de la collection Qdrant",
531
+ default="default",
532
+ ),
533
+ ParameterSpec(
534
+ name="vector_size",
535
+ kind="string",
536
+ prompt="Dimension obligatoire des vecteurs",
537
+ default="1536",
538
+ ),
539
+ ParameterSpec(
540
+ name="distance",
541
+ kind="string",
542
+ prompt="Métrique de similarité",
543
+ default="cosine",
544
+ choices=("cosine", "dot", "euclid"),
545
+ ),
546
+ ParameterSpec(
547
+ name="prefer_grpc",
548
+ kind="boolean",
549
+ prompt="Préférer le transport gRPC",
550
+ default=False,
551
+ ),
552
+ ParameterSpec(
553
+ name="timeout",
554
+ kind="string",
555
+ prompt="Timeout client en secondes",
556
+ default="5.0",
557
+ ),
558
+ ParameterSpec(
559
+ name="create_collection",
560
+ kind="boolean",
561
+ prompt="Créer la collection si elle manque",
562
+ default=True,
563
+ ),
564
+ ParameterSpec(
565
+ name="multitenant",
566
+ kind="boolean",
567
+ prompt="Résoudre url, api_key et collection par tenant",
568
+ default=False,
569
+ ),
570
+ ),
571
+ dependency_extra="qdrant",
572
+ entity_scoped=False,
573
+ ),
574
+ ),
575
+ )
@@ -81,7 +81,7 @@ def _framework_version() -> str:
81
81
  try:
82
82
  return version("arclith")
83
83
  except PackageNotFoundError:
84
- return "0.21.0"
84
+ return "0.22.0"
85
85
 
86
86
 
87
87
  def _create_package_layout(package_root: Path) -> None:
@@ -164,7 +164,7 @@ def add_adapter(
164
164
  "--capability",
165
165
  help=(
166
166
  "Capacité cible: repository, cache, logger, secrets, api, mcp, probe, http, "
167
- "command-bus, runtime, auth, tenant, license, llm, embedding, agent, "
167
+ "command-bus, runtime, auth, tenant, license, llm, embedding, vector-store, agent, "
168
168
  "agent-persistence ou observability"
169
169
  ),
170
170
  ),
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "arclith-cli"
7
- version = "0.18.0"
7
+ version = "0.19.0"
8
8
  description = "CLI scaffolding tool for arclith — hexagonal architecture framework"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -17,7 +17,7 @@ dependencies = [
17
17
  "typer>=0.15.0",
18
18
  "rich>=13.0.0",
19
19
  "httpx>=0.27.0",
20
- "arclith>=0.21.0",
20
+ "arclith>=0.22.0",
21
21
  ]
22
22
 
23
23
  [tool.uv.sources]
@@ -39,7 +39,7 @@ if grep -q '\[tool\.uv\.sources\]' pyproject.toml; then
39
39
  fi
40
40
 
41
41
  ARCLITH_VERSION=$(grep 'arclith\[' pyproject.toml | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')
42
- MIN_ARCLITH_VERSION="0.21.0"
42
+ MIN_ARCLITH_VERSION="0.22.0"
43
43
  if version_lt "$ARCLITH_VERSION" "$MIN_ARCLITH_VERSION"; then
44
44
  echo -e "${RED}✗ FAIL: arclith version < $MIN_ARCLITH_VERSION (found: $ARCLITH_VERSION)${NC}"
45
45
  exit 1
@@ -520,7 +520,7 @@ def test_add_langsmith_adds_dependency_extra_idempotently(tmp_path: Path) -> Non
520
520
  project_dir = _minimal_project(tmp_path)
521
521
  pyproject = project_dir / "pyproject.toml"
522
522
  pyproject.write_text(
523
- '[project]\ndependencies = ["arclith[fastapi,mcp]>=0.21.0"]\n',
523
+ '[project]\ndependencies = ["arclith[fastapi,mcp]>=0.22.0"]\n',
524
524
  encoding="utf-8",
525
525
  )
526
526
 
@@ -533,7 +533,7 @@ def test_add_langsmith_adds_dependency_extra_idempotently(tmp_path: Path) -> Non
533
533
  yes=True,
534
534
  )
535
535
 
536
- assert "arclith[fastapi,mcp,langsmith]>=0.21.0" in pyproject.read_text()
536
+ assert "arclith[fastapi,mcp,langsmith]>=0.22.0" in pyproject.read_text()
537
537
 
538
538
 
539
539
  def test_add_opentelemetry_adds_dependency_extra_idempotently(
@@ -542,7 +542,7 @@ def test_add_opentelemetry_adds_dependency_extra_idempotently(
542
542
  project_dir = _minimal_project(tmp_path)
543
543
  pyproject = project_dir / "pyproject.toml"
544
544
  pyproject.write_text(
545
- '[project]\ndependencies = ["arclith[fastapi,mcp]>=0.21.0"]\n',
545
+ '[project]\ndependencies = ["arclith[fastapi,mcp]>=0.22.0"]\n',
546
546
  encoding="utf-8",
547
547
  )
548
548
 
@@ -555,7 +555,7 @@ def test_add_opentelemetry_adds_dependency_extra_idempotently(
555
555
  yes=True,
556
556
  )
557
557
 
558
- assert "arclith[fastapi,mcp,opentelemetry]>=0.21.0" in pyproject.read_text()
558
+ assert "arclith[fastapi,mcp,opentelemetry]>=0.22.0" in pyproject.read_text()
559
559
 
560
560
 
561
561
  @pytest.mark.parametrize(
@@ -1701,7 +1701,7 @@ def test_add_langgraph_persistence_enriches_config_and_dependency_without_overwr
1701
1701
  (project_dir / "pyproject.toml").write_text(
1702
1702
  """[project]
1703
1703
  name = "demo-service"
1704
- dependencies = ["arclith[fastapi,mcp]>=0.21.0"]
1704
+ dependencies = ["arclith[fastapi,mcp]>=0.22.0"]
1705
1705
  """,
1706
1706
  encoding="utf-8",
1707
1707
  )
@@ -1753,7 +1753,7 @@ dependencies = ["arclith[fastapi,mcp]>=0.21.0"]
1753
1753
  )
1754
1754
  pyproject = (project_dir / "pyproject.toml").read_text(encoding="utf-8")
1755
1755
  assert (
1756
- "arclith[fastapi,mcp,langgraph,langgraph-persistence-mongodb]>=0.21.0"
1756
+ "arclith[fastapi,mcp,langgraph,langgraph-persistence-mongodb]>=0.22.0"
1757
1757
  in pyproject
1758
1758
  )
1759
1759
 
@@ -1775,7 +1775,7 @@ def test_add_langgraph_persistence_requires_agent_capability(
1775
1775
  ) -> None:
1776
1776
  project_dir = _minimal_project(tmp_path)
1777
1777
  (project_dir / "pyproject.toml").write_text(
1778
- '[project]\ndependencies = ["arclith>=0.21.0"]\n', encoding="utf-8"
1778
+ '[project]\ndependencies = ["arclith>=0.22.0"]\n', encoding="utf-8"
1779
1779
  )
1780
1780
 
1781
1781
  with pytest.raises(typer.Exit):
@@ -52,7 +52,7 @@ def test_init_project_creates_minimal_src_layout_without_entity(tmp_path: Path)
52
52
  "observability:\n"
53
53
  " enabled: []\n"
54
54
  )
55
- assert "arclith[fastapi,mcp]>=0.21.0" in (generated / "pyproject.toml").read_text(encoding="utf-8")
55
+ assert "arclith[fastapi,mcp]>=0.22.0" in (generated / "pyproject.toml").read_text(encoding="utf-8")
56
56
  assert (package_root / "domain" / "models" / "__init__.py").exists()
57
57
  assert (package_root / "domain" / "ports" / "inbound" / "__init__.py").exists()
58
58
  assert (package_root / "domain" / "ports" / "outbound" / "__init__.py").exists()
@@ -15,7 +15,7 @@ def _minimal_project(tmp_path: Path) -> Path:
15
15
  )
16
16
  (project_dir / "pyproject.toml").write_text(
17
17
  '[project]\nname = "demo-service"\nversion = "0.1.0"\n'
18
- 'dependencies = ["arclith>=0.21.0"]\n',
18
+ 'dependencies = ["arclith>=0.22.0"]\n',
19
19
  encoding="utf-8",
20
20
  )
21
21
  return project_dir
@@ -27,7 +27,11 @@ def test_embedding_capability_catalog_declares_deterministic_adapter() -> None:
27
27
  assert capability is not None
28
28
  assert capability.layer == "outbound"
29
29
  assert capability.activation_config_key is None
30
- assert capability.adapter_names() == ("deterministic", "openai-compatible")
30
+ assert capability.adapter_names() == (
31
+ "deterministic",
32
+ "openai-compatible",
33
+ "openai",
34
+ )
31
35
  deterministic = capability.get_adapter("deterministic")
32
36
  assert deterministic is not None
33
37
  assert deterministic.entity_scoped is False
@@ -53,6 +57,29 @@ def test_embedding_capability_catalog_declares_deterministic_adapter() -> None:
53
57
  "normalize",
54
58
  "multitenant",
55
59
  ]
60
+ openai = capability.get_adapter("openai")
61
+ assert openai is not None
62
+ assert openai.entity_scoped is False
63
+ assert openai.dependency_extra == "embedding"
64
+ assert openai.secret_mappings[0].field_path == "adapters.embedding.api_key"
65
+ assert openai.secret_mappings[0].secret_key == "OPENAI_API_KEY"
66
+ assert [parameter.name for parameter in openai.parameters] == [
67
+ "base_url",
68
+ "api_key",
69
+ "model_name",
70
+ "dimensions",
71
+ "batch_size",
72
+ "timeout",
73
+ "encoding_format",
74
+ "normalize",
75
+ "multitenant",
76
+ ]
77
+ assert (
78
+ next(
79
+ parameter for parameter in openai.parameters if parameter.name == "api_key"
80
+ ).secret
81
+ is True
82
+ )
56
83
 
57
84
 
58
85
  def test_embedding_capability_is_exposed_in_json_catalog() -> None:
@@ -65,6 +92,20 @@ def test_embedding_capability_is_exposed_in_json_catalog() -> None:
65
92
  assert [adapter["name"] for adapter in embedding["adapters"]] == [
66
93
  "deterministic",
67
94
  "openai-compatible",
95
+ "openai",
96
+ ]
97
+ openai = embedding["adapters"][2]
98
+ api_key = next(
99
+ parameter
100
+ for parameter in openai["parameters"]
101
+ if parameter["name"] == "api_key"
102
+ )
103
+ assert api_key["secret"] is True
104
+ assert openai["secret_mappings"] == [
105
+ {
106
+ "field_path": "adapters.embedding.api_key",
107
+ "secret_key": "OPENAI_API_KEY",
108
+ }
68
109
  ]
69
110
 
70
111
 
@@ -156,6 +197,87 @@ def test_add_openai_compatible_embedding_generates_loadable_config_idempotently(
156
197
  assert app.config.adapters.embedding is not None
157
198
  assert app.config.adapters.embedding.model_name == "nomic-embed-text"
158
199
  assert isinstance(app.embedding(), OpenAICompatibleEmbeddingAdapter)
159
- assert "arclith[embedding]>=0.21.0" in (project_dir / "pyproject.toml").read_text(
200
+ assert "arclith[embedding]>=0.22.0" in (project_dir / "pyproject.toml").read_text(
160
201
  encoding="utf-8"
161
202
  )
203
+
204
+
205
+ def test_add_openai_embedding_generates_safe_loadable_config_idempotently(
206
+ tmp_path: Path,
207
+ monkeypatch,
208
+ ) -> None:
209
+ project_dir = _minimal_project(tmp_path)
210
+ (project_dir / ".env").write_text("EXISTING=value\n", encoding="utf-8")
211
+ params = {
212
+ "model_name": "configured-embedding-model",
213
+ "dimensions": "1536",
214
+ }
215
+
216
+ for _ in range(2):
217
+ add_adapter_cmd(
218
+ project_dir=project_dir,
219
+ capability_name="embedding",
220
+ adapter="openai",
221
+ adapter_params=params,
222
+ yes=True,
223
+ )
224
+
225
+ config_path = project_dir / "config/adapters/outbound/embedding.yaml"
226
+ config_text = config_path.read_text(encoding="utf-8")
227
+ secrets_text = (project_dir / "config/secrets.yaml").read_text(encoding="utf-8")
228
+ env_text = (project_dir / ".env").read_text(encoding="utf-8")
229
+
230
+ assert config_text == (
231
+ "adapter: openai\n"
232
+ 'base_url: "https://api.openai.com/v1"\n'
233
+ "api_key: null\n"
234
+ 'model_name: "configured-embedding-model"\n'
235
+ "dimensions: 1536\n"
236
+ "batch_size: 64\n"
237
+ "timeout: 30.0\n"
238
+ "encoding_format: float\n"
239
+ "normalize: false\n"
240
+ "multitenant: false\n"
241
+ )
242
+ assert "adapters.embedding.api_key: OPENAI_API_KEY" in secrets_text
243
+ assert secrets_text.count("adapters.embedding.api_key") == 1
244
+ assert env_text == "EXISTING=value\n"
245
+ assert "sk-" not in config_text + secrets_text + env_text
246
+ assert ".env" in (project_dir / ".gitignore").read_text(encoding="utf-8")
247
+ assert "arclith[embedding]>=0.22.0" in (project_dir / "pyproject.toml").read_text(
248
+ encoding="utf-8"
249
+ )
250
+
251
+ monkeypatch.setenv("OPENAI_API_KEY", "test-key")
252
+ from arclith import Arclith
253
+ from arclith.adapters.outbound.openai import OpenAIEmbeddingAdapter
254
+
255
+ app = Arclith(project_dir / "config")
256
+
257
+ assert app.config.adapters.embedding is not None
258
+ assert app.config.adapters.embedding.api_key == "test-key"
259
+ assert type(app.embedding()) is OpenAIEmbeddingAdapter
260
+
261
+
262
+ def test_add_openai_embedding_omits_fake_key_and_keeps_dimensions_optional(
263
+ tmp_path: Path,
264
+ ) -> None:
265
+ project_dir = _minimal_project(tmp_path)
266
+
267
+ add_adapter_cmd(
268
+ project_dir=project_dir,
269
+ capability_name="embedding",
270
+ adapter="openai",
271
+ adapter_params={"model_name": "configured-embedding-model"},
272
+ yes=True,
273
+ )
274
+
275
+ config_text = (project_dir / "config/adapters/outbound/embedding.yaml").read_text(
276
+ encoding="utf-8"
277
+ )
278
+ env_text = (project_dir / ".env").read_text(encoding="utf-8")
279
+
280
+ assert "dimensions: null" in config_text
281
+ assert "api_key: null" in config_text
282
+ assert "OPENAI_API_KEY=" not in env_text
283
+ assert "sk-" not in config_text + env_text
@@ -0,0 +1,166 @@
1
+ from pathlib import Path
2
+
3
+ from arclith_cli.add_adapter import add_adapter_cmd
4
+ from arclith_cli.capabilities import capability_catalog_as_dict, get_capability
5
+
6
+
7
+ def _minimal_project(tmp_path: Path) -> Path:
8
+ project_dir = tmp_path / "demo-service"
9
+ (project_dir / "src" / "demo_service" / "domain" / "models").mkdir(parents=True)
10
+ config_dir = project_dir / "config" / "adapters"
11
+ config_dir.mkdir(parents=True)
12
+ (config_dir / "adapters.yaml").write_text(
13
+ "logger: console\nrepository: memory\nobservability:\n enabled: []\n",
14
+ encoding="utf-8",
15
+ )
16
+ (project_dir / "pyproject.toml").write_text(
17
+ '[project]\nname = "demo-service"\nversion = "0.1.0"\n'
18
+ 'dependencies = ["arclith>=0.22.0"]\n',
19
+ encoding="utf-8",
20
+ )
21
+ return project_dir
22
+
23
+
24
+ def test_vector_store_capability_catalog_declares_memory_adapter() -> None:
25
+ capability = get_capability("vector-store")
26
+
27
+ assert capability is not None
28
+ assert capability.layer == "outbound"
29
+ assert capability.activation_config_key is None
30
+ assert capability.adapter_names() == ("memory", "qdrant")
31
+ memory = capability.get_adapter("memory")
32
+ assert memory is not None
33
+ assert memory.entity_scoped is False
34
+ assert memory.dependency_extra is None
35
+ assert memory.config_path == "config/adapters/outbound/vector_store.yaml"
36
+ assert [parameter.name for parameter in memory.parameters] == [
37
+ "collection_name",
38
+ "vector_size",
39
+ "distance",
40
+ "multitenant",
41
+ ]
42
+
43
+
44
+ def test_vector_store_capability_is_exposed_in_json_catalog() -> None:
45
+ payload_by_name = {
46
+ capability["name"]: capability for capability in capability_catalog_as_dict()
47
+ }
48
+
49
+ vector_store = payload_by_name["vector-store"]
50
+ assert vector_store["activation_config_key"] is None
51
+ assert [adapter["name"] for adapter in vector_store["adapters"]] == [
52
+ "memory",
53
+ "qdrant",
54
+ ]
55
+
56
+
57
+ def test_vector_store_capability_declares_qdrant_adapter() -> None:
58
+ capability = get_capability("vector-store")
59
+
60
+ assert capability is not None
61
+ qdrant = capability.get_adapter("qdrant")
62
+ assert qdrant is not None
63
+ assert qdrant.entity_scoped is False
64
+ assert qdrant.dependency_extra == "qdrant"
65
+ assert qdrant.config_path == "config/adapters/outbound/vector_store.yaml"
66
+ assert [mapping.field_path for mapping in qdrant.secret_mappings] == [
67
+ "adapters.vector_store.api_key",
68
+ ]
69
+ assert [mapping.secret_key for mapping in qdrant.secret_mappings] == [
70
+ "QDRANT_API_KEY",
71
+ ]
72
+
73
+
74
+ def test_add_memory_vector_store_generates_loadable_config_idempotently(
75
+ tmp_path: Path,
76
+ ) -> None:
77
+ project_dir = _minimal_project(tmp_path)
78
+ params = {
79
+ "collection_name": "documents",
80
+ "vector_size": "3",
81
+ "distance": "cosine",
82
+ "multitenant": "false",
83
+ }
84
+
85
+ for _ in range(2):
86
+ add_adapter_cmd(
87
+ project_dir=project_dir,
88
+ capability_name="vector-store",
89
+ adapter="memory",
90
+ adapter_params=params,
91
+ yes=True,
92
+ )
93
+
94
+ from arclith import Arclith, MemoryVectorStore
95
+
96
+ config_path = project_dir / "config/adapters/outbound/vector_store.yaml"
97
+ app = Arclith(project_dir / "config")
98
+
99
+ assert config_path.read_text(encoding="utf-8") == (
100
+ "adapter: memory\n"
101
+ 'collection_name: "documents"\n'
102
+ "vector_size: 3\n"
103
+ "distance: cosine\n"
104
+ "multitenant: false\n"
105
+ )
106
+ assert app.config.adapters.vector_store is not None
107
+ assert app.config.adapters.vector_store.vector_size == 3
108
+ assert isinstance(app.vector_store(), MemoryVectorStore)
109
+ assert "vector_store:" not in (
110
+ project_dir / "config/adapters/adapters.yaml"
111
+ ).read_text(encoding="utf-8")
112
+
113
+
114
+ def test_add_qdrant_vector_store_generates_loadable_config_and_secrets(
115
+ tmp_path: Path,
116
+ monkeypatch,
117
+ ) -> None:
118
+ project_dir = _minimal_project(tmp_path)
119
+ params = {
120
+ "url": "http://localhost:6333",
121
+ "collection_name": "documents",
122
+ "vector_size": "1536",
123
+ "distance": "cosine",
124
+ "prefer_grpc": "false",
125
+ "timeout": "5.0",
126
+ "create_collection": "true",
127
+ "multitenant": "false",
128
+ }
129
+
130
+ for _ in range(2):
131
+ add_adapter_cmd(
132
+ project_dir=project_dir,
133
+ capability_name="vector-store",
134
+ adapter="qdrant",
135
+ adapter_params=params,
136
+ yes=True,
137
+ )
138
+
139
+ config_text = (
140
+ project_dir / "config/adapters/outbound/vector_store.yaml"
141
+ ).read_text(encoding="utf-8")
142
+ secrets_text = (project_dir / "config/secrets.yaml").read_text(encoding="utf-8")
143
+
144
+ assert config_text == (
145
+ "adapter: qdrant\n"
146
+ 'url: "http://localhost:6333"\n'
147
+ "api_key: null\n"
148
+ 'collection_name: "documents"\n'
149
+ "vector_size: 1536\n"
150
+ "distance: cosine\n"
151
+ "prefer_grpc: false\n"
152
+ "timeout: 5.0\n"
153
+ "create_collection: true\n"
154
+ "multitenant: false\n"
155
+ )
156
+ assert secrets_text.count("adapters.vector_store.api_key") == 1
157
+ assert "adapters.vector_store.api_key: QDRANT_API_KEY" in secrets_text
158
+ assert "adapters.vector_store.url" not in secrets_text
159
+
160
+ from arclith import Arclith, QdrantVectorStore
161
+
162
+ monkeypatch.setenv("QDRANT_API_KEY", "test-key")
163
+ app = Arclith(project_dir / "config")
164
+ assert isinstance(app.vector_store(), QdrantVectorStore)
165
+ assert app.config.adapters.vector_store is not None
166
+ assert app.config.adapters.vector_store.api_key == "test-key"
@@ -46,7 +46,7 @@ wheels = [
46
46
 
47
47
  [[package]]
48
48
  name = "arclith"
49
- version = "0.21.0"
49
+ version = "0.22.0"
50
50
  source = { editable = "../" }
51
51
  dependencies = [
52
52
  { name = "fastapi" },
@@ -130,6 +130,8 @@ requires-dist = [
130
130
  { name = "pytz", marker = "extra == 'all'", specifier = "==2024.1" },
131
131
  { name = "pytz", marker = "extra == 'duckdb'", specifier = "==2024.1" },
132
132
  { name = "pyyaml", specifier = "==6.0.3" },
133
+ { name = "qdrant-client", marker = "extra == 'all'", specifier = "==1.19.0" },
134
+ { name = "qdrant-client", marker = "extra == 'qdrant'", specifier = "==1.19.0" },
133
135
  { name = "redis", marker = "extra == 'all'", specifier = ">=5.0.0" },
134
136
  { name = "redis", marker = "extra == 'cache'", specifier = ">=5.0.0" },
135
137
  { name = "redis", marker = "extra == 'langgraph-runtime'", specifier = ">=5.0.0" },
@@ -141,7 +143,7 @@ requires-dist = [
141
143
  { name = "uvicorn", marker = "extra == 'fastapi'", specifier = "==0.41.0" },
142
144
  { name = "uvicorn", marker = "extra == 'langgraph-runtime'", specifier = "==0.41.0" },
143
145
  ]
144
- provides-extras = ["mongodb", "duckdb", "mariadb", "postgresql", "fastapi", "mcp", "vault", "cache", "auth", "embedding", "rabbitmq", "langgraph", "langgraph-runtime", "langgraph-persistence-sqlite", "langgraph-persistence-postgresql", "langgraph-persistence-mongodb", "langgraph-persistence-redis", "langsmith", "opentelemetry", "all", "azure-blob", "s3", "gcs"]
146
+ provides-extras = ["mongodb", "duckdb", "mariadb", "postgresql", "fastapi", "mcp", "vault", "cache", "auth", "embedding", "rabbitmq", "langgraph", "langgraph-runtime", "langgraph-persistence-sqlite", "langgraph-persistence-postgresql", "langgraph-persistence-mongodb", "langgraph-persistence-redis", "langsmith", "opentelemetry", "all", "azure-blob", "s3", "gcs", "qdrant"]
145
147
 
146
148
  [package.metadata.requires-dev]
147
149
  dev = [
@@ -163,7 +165,7 @@ docs = [
163
165
 
164
166
  [[package]]
165
167
  name = "arclith-cli"
166
- version = "0.18.0"
168
+ version = "0.19.0"
167
169
  source = { editable = "." }
168
170
  dependencies = [
169
171
  { name = "arclith" },
@@ -1 +0,0 @@
1
- __version__ = "0.18.0"
File without changes
File without changes
File without changes
File without changes