Coverage for src/qdrant_loader/core/conversion/settings.py: 91%

34 statements  

« prev     ^ index     » next       coverage.py v7.15.0, created at 2026-07-20 10:15 +0000

1"""The loader-config surface for the docling engine (pydantic) and its translation 

2into the engine-internal frozen :class:`~.config.ConversionConfig`. 

3 

4The rest of qdrant-loader configures conversion through YAML, which is parsed into 

5pydantic models (validation, env interpolation, the existing settings tree). The 

6engine, by contrast, takes a *frozen* dataclass that knows nothing about pydantic. 

7This module is the one-way bridge between the two — the anti-corruption boundary that 

8keeps the engine swappable: pydantic stays in the config layer, the frozen config 

9stays in the engine, and ``to_config`` is the only crossing. 

10 

11Only a curated subset of knobs is surfaced here; the rest come from the chosen 

12:class:`~.config.ConversionProfile`. Surfacing more is a mechanical extension the TDD 

13pass can drive as real configs demand it. 

14""" 

15 

16from __future__ import annotations 

17 

18from pydantic import BaseModel, Field 

19 

20from .config import ( 

21 ConversionConfig, 

22 ConversionProfile, 

23 PictureDescriptionConfig, 

24) 

25 

26 

27class DoclingPictureSettings(BaseModel): 

28 """YAML surface for API image-captioning, mapped to :class:`PictureDescriptionConfig`. 

29 

30 Off by default. When enabled it drives docling's remote picture-description 

31 pipeline; ``api_key`` is typically supplied via ``${ENV_VAR}`` interpolation. 

32 """ 

33 

34 enabled: bool = Field(default=False) 

35 url: str = Field(default="https://api.openai.com/v1/chat/completions") 

36 model: str = Field(default="gpt-4o-mini") 

37 api_key: str | None = Field(default=None) 

38 prompt: str = Field(default="Describe this image in a few sentences.") 

39 timeout: float = Field(default=60.0, gt=0) 

40 concurrency: int = Field(default=1, gt=0) 

41 

42 def to_config(self) -> PictureDescriptionConfig: 

43 """Project these settings into the engine's frozen picture config.""" 

44 return PictureDescriptionConfig( 

45 enabled=self.enabled, 

46 url=self.url, 

47 model=self.model, 

48 api_key=self.api_key, 

49 prompt=self.prompt, 

50 timeout=self.timeout, 

51 concurrency=self.concurrency, 

52 ) 

53 

54 

55class DoclingConversionSettings(BaseModel): 

56 """YAML surface for the docling conversion engine. 

57 

58 A :class:`~.config.ConversionProfile` selects the baseline (cost vs. fidelity); 

59 the optional fields below override individual top-level knobs on top of it. Only 

60 fields the user *explicitly sets* override the profile — unset fields keep the 

61 profile's value, so ``profile: accurate`` is not silently reset to the schema 

62 defaults. 

63 """ 

64 

65 profile: ConversionProfile = Field(default=ConversionProfile.FAST) 

66 

67 # Optional top-level overrides (unset => inherit from the profile baseline). 

68 max_file_size: int | None = Field(default=None, gt=0) 

69 document_timeout: float | None = Field(default=None) 

70 enabled_formats: tuple[str, ...] | None = Field(default=None) 

71 device: str | None = Field(default=None) 

72 num_threads: int | None = Field(default=None, gt=0) 

73 compile_models: bool | None = Field(default=None) 

74 artifacts_path: str | None = Field(default=None) 

75 

76 picture: DoclingPictureSettings = Field(default_factory=DoclingPictureSettings) 

77 

78 def to_config(self) -> ConversionConfig: 

79 """Translate into the frozen engine config: profile baseline + set overrides.""" 

80 explicitly_set = self.model_fields_set 

81 overrides: dict[str, object] = {} 

82 

83 for name in ( 

84 "max_file_size", 

85 "document_timeout", 

86 "device", 

87 "num_threads", 

88 "compile_models", 

89 "artifacts_path", 

90 ): 

91 if name in explicitly_set: 

92 overrides[name] = getattr(self, name) 

93 

94 if "enabled_formats" in explicitly_set and self.enabled_formats is not None: 

95 # The frozen config stores a tuple; pydantic may hand back a list. 

96 overrides["enabled_formats"] = tuple(self.enabled_formats) 

97 

98 if "picture" in explicitly_set: 

99 overrides["picture"] = self.picture.to_config() 

100 

101 return ConversionConfig.from_profile(self.profile, **overrides)