contextgem 0.25.0__tar.gz → 0.26.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 (74) hide show
  1. {contextgem-0.25.0 → contextgem-0.26.0}/.gitignore +3 -0
  2. contextgem-0.26.0/LICENSES.md +115 -0
  3. {contextgem-0.25.0 → contextgem-0.26.0}/PKG-INFO +7 -5
  4. {contextgem-0.25.0 → contextgem-0.26.0}/README.md +2 -1
  5. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/__init__.py +1 -1
  6. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/internal/base/aspects.py +22 -0
  7. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/internal/base/documents.py +79 -2
  8. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/internal/base/llms.py +20 -20
  9. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/internal/base/serialization.py +43 -2
  10. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/internal/prompts/extract_aspect_items.j2 +3 -3
  11. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/internal/tools/schema_generator.py +83 -19
  12. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/internal/typings/strings_to_types.py +19 -8
  13. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/internal/typings/types.py +1 -1
  14. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/internal/typings/types_to_strings.py +39 -27
  15. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/public/llms.py +8 -3
  16. {contextgem-0.25.0 → contextgem-0.26.0}/pyproject.toml +8 -7
  17. {contextgem-0.25.0 → contextgem-0.26.0}/LICENSE +0 -0
  18. {contextgem-0.25.0 → contextgem-0.26.0}/NOTICE +0 -0
  19. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/internal/__init__.py +0 -0
  20. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/internal/base/__init__.py +0 -0
  21. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/internal/base/abstract.py +0 -0
  22. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/internal/base/attrs.py +0 -0
  23. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/internal/base/concepts.py +0 -0
  24. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/internal/base/data_models.py +0 -0
  25. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/internal/base/examples.py +0 -0
  26. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/internal/base/images.py +0 -0
  27. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/internal/base/instances.py +0 -0
  28. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/internal/base/items.py +0 -0
  29. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/internal/base/md_text.py +0 -0
  30. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/internal/base/mixins.py +0 -0
  31. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/internal/base/paras_and_sents.py +0 -0
  32. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/internal/base/pipelines.py +0 -0
  33. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/internal/base/utils.py +0 -0
  34. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/internal/converters/__init__.py +0 -0
  35. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/internal/converters/docx/__init__.py +0 -0
  36. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/internal/converters/docx/base.py +0 -0
  37. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/internal/converters/docx/namespaces.py +0 -0
  38. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/internal/converters/docx/package.py +0 -0
  39. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/internal/converters/docx/utils.py +0 -0
  40. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/internal/data_models.py +0 -0
  41. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/internal/decorators.py +0 -0
  42. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/internal/exceptions.py +0 -0
  43. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/internal/items.py +0 -0
  44. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/internal/llm_output_structs/__init__.py +0 -0
  45. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/internal/llm_output_structs/aspect_structs.py +0 -0
  46. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/internal/llm_output_structs/concept_structs.py +0 -0
  47. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/internal/llm_output_structs/utils.py +0 -0
  48. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/internal/loggers.py +0 -0
  49. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/internal/prompts/extract_concept_items.j2 +0 -0
  50. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/internal/registry.py +0 -0
  51. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/internal/suppressions.py +0 -0
  52. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/internal/system/default_system_message.j2 +0 -0
  53. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/internal/tools/__init__.py +0 -0
  54. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/internal/tools/docstring_parser.py +0 -0
  55. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/internal/typings/__init__.py +0 -0
  56. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/internal/typings/typed_class_utils.py +0 -0
  57. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/internal/typings/types_normalization.py +0 -0
  58. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/internal/typings/user_type_hints_validation.py +0 -0
  59. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/internal/typings/validators.py +0 -0
  60. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/internal/utils.py +0 -0
  61. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/public/__init__.py +0 -0
  62. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/public/aspects.py +0 -0
  63. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/public/concepts.py +0 -0
  64. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/public/converters/__init__.py +0 -0
  65. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/public/converters/docx.py +0 -0
  66. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/public/data_models.py +0 -0
  67. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/public/decorators.py +0 -0
  68. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/public/documents.py +0 -0
  69. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/public/examples.py +0 -0
  70. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/public/images.py +0 -0
  71. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/public/paragraphs.py +0 -0
  72. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/public/pipelines.py +0 -0
  73. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/public/sentences.py +0 -0
  74. {contextgem-0.25.0 → contextgem-0.26.0}/contextgem/public/utils.py +0 -0
@@ -24,3 +24,6 @@ dist
24
24
  code_sample.py
25
25
 
26
26
  settings.local.json
27
+
28
+ .claude
29
+ .agents
@@ -0,0 +1,115 @@
1
+ # License Analysis Report
2
+
3
+ **Project license:** Apache-2.0
4
+
5
+ |Package|Ecosystem|Group|Source|License|Risk|Status|
6
+ |---------|-----------|-------|--------|---------|------|--------|
7
+ |[aiolimiter (1.2.1)](https://pypi.org/project/aiolimiter/)|python|prod|[pyproject.toml](pyproject.toml)|[MIT](https://spdx.org/licenses/MIT.html)|permissive|✓|
8
+ |[colorlog (6.12.0)](https://pypi.org/project/colorlog/)|python|prod|[pyproject.toml](pyproject.toml)|[MIT](https://spdx.org/licenses/MIT.html)|permissive|✓|
9
+ | └─ [colorama (0.4.6)](https://pypi.org/project/colorama/) (also: litellm, openai +1 more)|python|prod||[BSD-3-Clause](https://spdx.org/licenses/BSD-3-Clause.html)|permissive|✓|
10
+ |[docstring-parser (0.18.0)](https://pypi.org/project/docstring-parser/)|python|prod|[pyproject.toml](pyproject.toml)|[MIT](https://spdx.org/licenses/MIT.html)|permissive|✓|
11
+ |[fastjsonschema (2.22.1)](https://pypi.org/project/fastjsonschema/)|python|prod|[pyproject.toml](pyproject.toml)|[BSD-3-Clause](https://spdx.org/licenses/BSD-3-Clause.html)|permissive|✓|
12
+ |[genai-prices (0.0.72)](https://pypi.org/project/genai-prices/)|python|prod|[pyproject.toml](pyproject.toml)|[MIT](https://spdx.org/licenses/MIT.html)|permissive|✓|
13
+ | └─ [annotated-types (0.8.0)](https://pypi.org/project/annotated-types/) (also: litellm, openai +1 more)|python|prod||[MIT](https://spdx.org/licenses/MIT.html)|permissive|✓|
14
+ | └─ [anyio (4.14.2)](https://pypi.org/project/anyio/) (also: litellm, openai +1 more)|python|prod||[MIT](https://spdx.org/licenses/MIT.html)|permissive|✓|
15
+ | └─ [exceptiongroup (1.3.1)](https://pypi.org/project/exceptiongroup/) (also: litellm, openai +1 more)|python|prod||[MIT](https://spdx.org/licenses/MIT.html)|permissive|✓|
16
+ | └─ [h11 (0.16.0)](https://pypi.org/project/h11/) (also: litellm, openai +1 more)|python|prod||[MIT](https://spdx.org/licenses/MIT.html)|permissive|✓|
17
+ | └─ [httpcore2 (2.9.1)](https://pypi.org/project/httpcore2/)|python|prod||[BSD-3-Clause](https://spdx.org/licenses/BSD-3-Clause.html)|permissive|✓|
18
+ | └─ [httpx2 (2.9.1)](https://pypi.org/project/httpx2/)|python|prod||[BSD-3-Clause](https://spdx.org/licenses/BSD-3-Clause.html)|permissive|✓|
19
+ | └─ [idna (3.18.0)](https://pypi.org/project/idna/) (also: litellm, openai +1 more)|python|prod||[BSD-3-Clause](https://spdx.org/licenses/BSD-3-Clause.html)|permissive|✓|
20
+ | └─ [pydantic-core (2.46.4)](https://pypi.org/project/pydantic-core/) (also: litellm, openai +1 more)|python|prod||[MIT](https://spdx.org/licenses/MIT.html)|permissive|✓|
21
+ | └─ [truststore (0.10.4)](https://pypi.org/project/truststore/)|python|prod||[MIT](https://spdx.org/licenses/MIT.html)|permissive|✓|
22
+ | └─ [typing-inspection (0.4.2)](https://pypi.org/project/typing-inspection/) (also: litellm, openai +1 more)|python|prod||[MIT](https://spdx.org/licenses/MIT.html)|permissive|✓|
23
+ |[jinja2 (3.1.6)](https://pypi.org/project/jinja2/)|python|prod|[pyproject.toml](pyproject.toml)|[BSD-3-Clause](https://spdx.org/licenses/BSD-3-Clause.html)|permissive|✓|
24
+ | └─ [markupsafe (3.0.3)](https://pypi.org/project/markupsafe/) (also: litellm)|python|prod||[BSD-3-Clause](https://spdx.org/licenses/BSD-3-Clause.html)|permissive|✓|
25
+ |[litellm (1.93.0)](https://pypi.org/project/litellm/)|python|prod|[pyproject.toml](pyproject.toml)|[MIT](https://spdx.org/licenses/MIT.html)|permissive|✓|
26
+ | └─ [aiohappyeyeballs (2.7.1)](https://pypi.org/project/aiohappyeyeballs/)|python|prod||[PSF-2.0](https://spdx.org/licenses/PSF-2.0.html)|permissive|✓|
27
+ | └─ [aiohttp (3.14.3)](https://pypi.org/project/aiohttp/)|python|prod||[Apache-2.0](https://spdx.org/licenses/Apache-2.0.html) AND [MIT](https://spdx.org/licenses/MIT.html)|permissive|✓|
28
+ | └─ [aiosignal (1.4.0)](https://pypi.org/project/aiosignal/)|python|prod||[Apache-2.0](https://spdx.org/licenses/Apache-2.0.html)|permissive|✓|
29
+ | └─ [async-timeout (5.0.1)](https://pypi.org/project/async-timeout/)|python|prod||[Apache-2.0](https://spdx.org/licenses/Apache-2.0.html)|permissive|✓|
30
+ | └─ [attrs (26.1.0)](https://pypi.org/project/attrs/)|python|prod||[MIT](https://spdx.org/licenses/MIT.html)|permissive|✓|
31
+ | └─ [certifi (2026.7.22)](https://pypi.org/project/certifi/) (also: openai, wtpsplit-lite)|python|prod||[MPL-2.0](https://spdx.org/licenses/MPL-2.0.html)|weak-copyleft|⚠|
32
+ | └─ [charset-normalizer (3.4.9)](https://pypi.org/project/charset-normalizer/)|python|prod||[MIT](https://spdx.org/licenses/MIT.html)|permissive|✓|
33
+ | └─ [click (8.4.2)](https://pypi.org/project/click/) (also: wtpsplit-lite)|python|prod||[BSD-3-Clause](https://spdx.org/licenses/BSD-3-Clause.html)|permissive|✓|
34
+ | └─ [distro (1.9.0)](https://pypi.org/project/distro/) (also: openai)|python|prod||[Apache-2.0](https://spdx.org/licenses/Apache-2.0.html)|permissive|✓|
35
+ | └─ [fastuuid (0.14.0)](https://pypi.org/project/fastuuid/)|python|prod||[BSD-3-Clause](https://spdx.org/licenses/BSD-3-Clause.html)|permissive|✓|
36
+ | └─ [filelock (3.32.0)](https://pypi.org/project/filelock/) (also: wtpsplit-lite)|python|prod||[MIT](https://spdx.org/licenses/MIT.html)|permissive|✓|
37
+ | └─ [frozenlist (1.8.0)](https://pypi.org/project/frozenlist/)|python|prod||[Apache-2.0](https://spdx.org/licenses/Apache-2.0.html)|permissive|✓|
38
+ | └─ [fsspec (2026.7.0)](https://pypi.org/project/fsspec/) (also: wtpsplit-lite)|python|prod||[BSD-3-Clause](https://spdx.org/licenses/BSD-3-Clause.html)|permissive|✓|
39
+ | └─ [hf-xet (1.5.2)](https://pypi.org/project/hf-xet/) (also: wtpsplit-lite)|python|prod||[Apache-2.0](https://spdx.org/licenses/Apache-2.0.html)|permissive|✓|
40
+ | └─ [httpcore (1.0.9)](https://pypi.org/project/httpcore/) (also: openai, wtpsplit-lite)|python|prod||[BSD-3-Clause](https://spdx.org/licenses/BSD-3-Clause.html)|permissive|✓|
41
+ | └─ [httpx (0.28.1)](https://pypi.org/project/httpx/) (also: openai, wtpsplit-lite)|python|prod||[BSD-3-Clause](https://spdx.org/licenses/BSD-3-Clause.html)|permissive|✓|
42
+ | └─ [huggingface-hub (1.25.1)](https://pypi.org/project/huggingface-hub/) (also: wtpsplit-lite)|python|prod||[Apache-2.0](https://spdx.org/licenses/Apache-2.0.html)|permissive|✓|
43
+ | └─ [importlib-metadata (8.9.0)](https://pypi.org/project/importlib-metadata/)|python|prod||[Apache-2.0](https://spdx.org/licenses/Apache-2.0.html)|permissive|✓|
44
+ | └─ [jiter (0.16.0)](https://pypi.org/project/jiter/) (also: openai)|python|prod||[MIT](https://spdx.org/licenses/MIT.html)|permissive|✓|
45
+ | └─ [jsonschema (4.26.0)](https://pypi.org/project/jsonschema/)|python|prod||[MIT](https://spdx.org/licenses/MIT.html)|permissive|✓|
46
+ | └─ [jsonschema-specifications (2025.9.1)](https://pypi.org/project/jsonschema-specifications/)|python|prod||[MIT](https://spdx.org/licenses/MIT.html)|permissive|✓|
47
+ | └─ [multidict (6.7.1)](https://pypi.org/project/multidict/)|python|prod||[Apache-2.0](https://spdx.org/licenses/Apache-2.0.html)|permissive|✓|
48
+ | └─ [packaging (26.2.0)](https://pypi.org/project/packaging/) (also: onnxruntime, wtpsplit-lite)|python|prod||[Apache-2.0](https://spdx.org/licenses/Apache-2.0.html) OR [BSD-2-Clause](https://spdx.org/licenses/BSD-2-Clause.html)|permissive|✓|
49
+ | └─ [propcache (0.5.2)](https://pypi.org/project/propcache/)|python|prod||[Apache-2.0](https://spdx.org/licenses/Apache-2.0.html)|permissive|✓|
50
+ | └─ [pyyaml (6.0.3)](https://pypi.org/project/pyyaml/) (also: wtpsplit-lite)|python|prod||[MIT](https://spdx.org/licenses/MIT.html)|permissive|✓|
51
+ | └─ [referencing (0.37.0)](https://pypi.org/project/referencing/)|python|prod||[MIT](https://spdx.org/licenses/MIT.html)|permissive|✓|
52
+ | └─ [regex (2026.7.19)](https://pypi.org/project/regex/)|python|prod||[Apache-2.0](https://spdx.org/licenses/Apache-2.0.html) AND [CNRI-Python](https://spdx.org/licenses/CNRI-Python.html)|unknown|?|
53
+ | └─ [requests (2.34.2)](https://pypi.org/project/requests/)|python|prod||[Apache-2.0](https://spdx.org/licenses/Apache-2.0.html)|permissive|✓|
54
+ | └─ [rpds-py (0.30.0)](https://pypi.org/project/rpds-py/)|python|prod||[MIT](https://spdx.org/licenses/MIT.html)|permissive|✓|
55
+ | └─ [rpds-py (2026.6.3)](https://pypi.org/project/rpds-py/)|python|prod||[MIT](https://spdx.org/licenses/MIT.html)|permissive|✓|
56
+ | └─ [sniffio (1.3.1)](https://pypi.org/project/sniffio/) (also: openai)|python|prod||[Apache-2.0](https://spdx.org/licenses/Apache-2.0.html) OR [MIT](https://spdx.org/licenses/MIT.html)|permissive|✓|
57
+ | └─ [tiktoken (0.13.0)](https://pypi.org/project/tiktoken/)|python|prod||[MIT](https://spdx.org/licenses/MIT.html)|permissive|✓|
58
+ | └─ [tokenizers (0.23.1)](https://pypi.org/project/tokenizers/) (also: wtpsplit-lite)|python|prod||[Apache-2.0](https://spdx.org/licenses/Apache-2.0.html)|permissive|✓|
59
+ | └─ [tqdm (4.70.0)](https://pypi.org/project/tqdm/) (also: openai, wtpsplit-lite)|python|prod||[MIT](https://spdx.org/licenses/MIT.html) AND [MPL-2.0](https://spdx.org/licenses/MPL-2.0.html)|weak-copyleft|⚠|
60
+ | └─ [urllib3 (2.7.0)](https://pypi.org/project/urllib3/)|python|prod||[MIT](https://spdx.org/licenses/MIT.html)|permissive|✓|
61
+ | └─ [yarl (1.24.5)](https://pypi.org/project/yarl/)|python|prod||[Apache-2.0](https://spdx.org/licenses/Apache-2.0.html)|permissive|✓|
62
+ | └─ [zipp (4.1.0)](https://pypi.org/project/zipp/)|python|prod||[MIT](https://spdx.org/licenses/MIT.html)|permissive|✓|
63
+ |[lxml (6.1.1)](https://pypi.org/project/lxml/)|python|prod|[pyproject.toml](pyproject.toml)|[BSD-3-Clause](https://spdx.org/licenses/BSD-3-Clause.html)|permissive|✓|
64
+ |[onnxruntime (1.23.2)](https://pypi.org/project/onnxruntime/)|python|prod|[pyproject.toml](pyproject.toml)|[MIT](https://spdx.org/licenses/MIT.html)|permissive|✓|
65
+ | └─ [coloredlogs (15.0.1)](https://pypi.org/project/coloredlogs/) (also: wtpsplit-lite)|python|prod||[MIT](https://spdx.org/licenses/MIT.html)|permissive|✓|
66
+ | └─ [flatbuffers (25.12.19)](https://pypi.org/project/flatbuffers/) (also: wtpsplit-lite)|python|prod||[Apache-2.0](https://spdx.org/licenses/Apache-2.0.html)|permissive|✓|
67
+ | └─ [humanfriendly (10.0.0)](https://pypi.org/project/humanfriendly/) (also: wtpsplit-lite)|python|prod||[MIT](https://spdx.org/licenses/MIT.html)|permissive|✓|
68
+ | └─ [mpmath (1.3.0)](https://pypi.org/project/mpmath/) (also: wtpsplit-lite)|python|prod||[BSD-3-Clause](https://spdx.org/licenses/BSD-3-Clause.html)|permissive|✓|
69
+ | └─ [numpy (2.2.6)](https://pypi.org/project/numpy/) (also: wtpsplit-lite)|python|prod||[BSD-3-Clause](https://spdx.org/licenses/BSD-3-Clause.html)|permissive|✓|
70
+ | └─ [numpy (2.4.6)](https://pypi.org/project/numpy/) (also: wtpsplit-lite)|python|prod||[0BSD](https://spdx.org/licenses/0BSD.html) AND [BSD-3-Clause](https://spdx.org/licenses/BSD-3-Clause.html) AND [CC0-1.0](https://spdx.org/licenses/CC0-1.0.html) AND [MIT](https://spdx.org/licenses/MIT.html) AND [Zlib](https://spdx.org/licenses/Zlib.html)|permissive|✓|
71
+ | └─ [numpy (2.5.1)](https://pypi.org/project/numpy/) (also: wtpsplit-lite)|python|prod||[0BSD](https://spdx.org/licenses/0BSD.html) AND [BSD-3-Clause](https://spdx.org/licenses/BSD-3-Clause.html) AND [CC0-1.0](https://spdx.org/licenses/CC0-1.0.html) AND [MIT](https://spdx.org/licenses/MIT.html) AND [Zlib](https://spdx.org/licenses/Zlib.html)|permissive|✓|
72
+ | └─ [protobuf (7.35.1)](https://pypi.org/project/protobuf/) (also: wtpsplit-lite)|python|prod||[BSD-3-Clause](https://spdx.org/licenses/BSD-3-Clause.html)|permissive|✓|
73
+ | └─ [pyreadline3 (3.5.6)](https://pypi.org/project/pyreadline3/) (also: wtpsplit-lite)|python|prod||[BSD-3-Clause](https://spdx.org/licenses/BSD-3-Clause.html)|permissive|✓|
74
+ | └─ [sympy (1.14.0)](https://pypi.org/project/sympy/) (also: wtpsplit-lite)|python|prod||[BSD-3-Clause](https://spdx.org/licenses/BSD-3-Clause.html)|permissive|✓|
75
+ |[onnxruntime (1.28.0)](https://pypi.org/project/onnxruntime/)|python|prod|[pyproject.toml](pyproject.toml)|[MIT](https://spdx.org/licenses/MIT.html)|permissive|✓|
76
+ | └─ [coloredlogs (15.0.1)](https://pypi.org/project/coloredlogs/) (also: wtpsplit-lite)|python|prod||[MIT](https://spdx.org/licenses/MIT.html)|permissive|✓|
77
+ | └─ [flatbuffers (25.12.19)](https://pypi.org/project/flatbuffers/) (also: wtpsplit-lite)|python|prod||[Apache-2.0](https://spdx.org/licenses/Apache-2.0.html)|permissive|✓|
78
+ | └─ [humanfriendly (10.0.0)](https://pypi.org/project/humanfriendly/) (also: wtpsplit-lite)|python|prod||[MIT](https://spdx.org/licenses/MIT.html)|permissive|✓|
79
+ | └─ [mpmath (1.3.0)](https://pypi.org/project/mpmath/) (also: wtpsplit-lite)|python|prod||[BSD-3-Clause](https://spdx.org/licenses/BSD-3-Clause.html)|permissive|✓|
80
+ | └─ [numpy (2.2.6)](https://pypi.org/project/numpy/) (also: wtpsplit-lite)|python|prod||[BSD-3-Clause](https://spdx.org/licenses/BSD-3-Clause.html)|permissive|✓|
81
+ | └─ [numpy (2.4.6)](https://pypi.org/project/numpy/) (also: wtpsplit-lite)|python|prod||[0BSD](https://spdx.org/licenses/0BSD.html) AND [BSD-3-Clause](https://spdx.org/licenses/BSD-3-Clause.html) AND [CC0-1.0](https://spdx.org/licenses/CC0-1.0.html) AND [MIT](https://spdx.org/licenses/MIT.html) AND [Zlib](https://spdx.org/licenses/Zlib.html)|permissive|✓|
82
+ | └─ [numpy (2.5.1)](https://pypi.org/project/numpy/) (also: wtpsplit-lite)|python|prod||[0BSD](https://spdx.org/licenses/0BSD.html) AND [BSD-3-Clause](https://spdx.org/licenses/BSD-3-Clause.html) AND [CC0-1.0](https://spdx.org/licenses/CC0-1.0.html) AND [MIT](https://spdx.org/licenses/MIT.html) AND [Zlib](https://spdx.org/licenses/Zlib.html)|permissive|✓|
83
+ | └─ [protobuf (7.35.1)](https://pypi.org/project/protobuf/) (also: wtpsplit-lite)|python|prod||[BSD-3-Clause](https://spdx.org/licenses/BSD-3-Clause.html)|permissive|✓|
84
+ | └─ [pyreadline3 (3.5.6)](https://pypi.org/project/pyreadline3/) (also: wtpsplit-lite)|python|prod||[BSD-3-Clause](https://spdx.org/licenses/BSD-3-Clause.html)|permissive|✓|
85
+ | └─ [sympy (1.14.0)](https://pypi.org/project/sympy/) (also: wtpsplit-lite)|python|prod||[BSD-3-Clause](https://spdx.org/licenses/BSD-3-Clause.html)|permissive|✓|
86
+ |[openai (2.49.0)](https://pypi.org/project/openai/)|python|prod|[pyproject.toml](pyproject.toml)|[Apache-2.0](https://spdx.org/licenses/Apache-2.0.html)|permissive|✓|
87
+ |[pillow (11.3.0)](https://pypi.org/project/pillow/)|python|prod|[pyproject.toml](pyproject.toml)|[MIT-CMU](https://spdx.org/licenses/MIT-CMU.html)|permissive|✓|
88
+ |[pydantic (2.13.4)](https://pypi.org/project/pydantic/)|python|prod|[pyproject.toml](pyproject.toml)|[MIT](https://spdx.org/licenses/MIT.html)|permissive|✓|
89
+ |[python-dotenv (1.2.2)](https://pypi.org/project/python-dotenv/)|python|prod|[pyproject.toml](pyproject.toml)|[BSD-3-Clause](https://spdx.org/licenses/BSD-3-Clause.html)|permissive|✓|
90
+ |[python-ulid (3.2.1)](https://pypi.org/project/python-ulid/)|python|prod|[pyproject.toml](pyproject.toml)|[MIT](https://spdx.org/licenses/MIT.html)|permissive|✓|
91
+ |[tenacity (9.1.4)](https://pypi.org/project/tenacity/)|python|prod|[pyproject.toml](pyproject.toml)|[Apache-2.0](https://spdx.org/licenses/Apache-2.0.html)|permissive|✓|
92
+ |[tethered (0.5.1)](https://pypi.org/project/tethered/)|python|prod|[pyproject.toml](pyproject.toml)|[MIT](https://spdx.org/licenses/MIT.html)|permissive|✓|
93
+ |[typing-extensions (4.16.0)](https://pypi.org/project/typing-extensions/)|python|prod|[pyproject.toml](pyproject.toml)|[PSF-2.0](https://spdx.org/licenses/PSF-2.0.html)|permissive|✓|
94
+ |[wtpsplit-lite (0.2.0)](https://pypi.org/project/wtpsplit-lite/)|python|prod|[pyproject.toml](pyproject.toml)|[MIT](https://spdx.org/licenses/MIT.html)|permissive|✓|
95
+
96
+ **Summary:** 0 violations, 2 warnings, 1 unknown, 75 ok (of which 4 reviewed)
97
+
98
+ **Completed in:** 0.66s
99
+
100
+ ## Details
101
+
102
+ - ⚠ certifi uses MPL-2.0 — weak copyleft license — modifications to the licensed files must remain under the same license; linking from other files is allowed. Review whether this is acceptable for your Apache-2.0 project
103
+ - ? regex uses Apache-2.0 AND CNRI-Python — could not determine compatibility with Apache-2.0
104
+ - ⚠ tqdm uses MIT AND MPL-2.0 — weak copyleft license — modifications to the licensed files must remain under the same license; linking from other files is allowed. Review whether this is acceptable for your Apache-2.0 project
105
+
106
+ ## Reviewed
107
+
108
+ |Package|Detected|Reviewed|Note|
109
+ |---------|----------|----------|----|
110
+ |[certifi (2026.7.22)](https://pypi.org/project/certifi/)|MPL-2.0|[MPL-2.0](https://spdx.org/licenses/MPL-2.0.html)|Apache-2.0 library distributed via PyPI; certifi is an unmodified upstream dependency used with dynamic linkage. MPL-2.0 is file-level weak copyleft that only triggers on modification of the MPL-licensed files — no modifications, so the obligation is dormant.|
111
+ |[regex (2026.7.19)](https://pypi.org/project/regex/)|Apache-2.0 AND CNRI-Python|[Apache-2.0](https://spdx.org/licenses/Apache-2.0.html) AND [CNRI-Python](https://spdx.org/licenses/CNRI-Python.html)|Flagged UNKNOWN only because licenseal cannot classify the compound expression. CNRI-Python is an OSI-approved permissive license (the historical Python license) and is compatible with Apache-2.0; regex is used unmodified with dynamic linkage. Detection-classification override.|
112
+ |[tqdm (4.70.0)](https://pypi.org/project/tqdm/)|MIT AND MPL-2.0|[MIT](https://spdx.org/licenses/MIT.html) AND [MPL-2.0](https://spdx.org/licenses/MPL-2.0.html)|Same rationale as certifi: unmodified upstream dependency, dynamic linkage. The MPL-2.0 portion is file-level weak copyleft and does not trigger absent modification of the MPL-licensed files.|
113
+ |[wtpsplit-lite (0.2.0)](https://pypi.org/project/wtpsplit-lite/)|UNKNOWN|[MIT](https://spdx.org/licenses/MIT.html)|PyPI package ships no license metadata (hence UNKNOWN); the repository LICENSE file is MIT, confirmed via GitHub's license-detection API for the superlinear-ai/wtpsplit-lite repo (spdx_id MIT). Detection override — MIT is permissive and compatible with Apache-2.0.|
114
+
115
+ _Generated by `licenseal check`. Versions shown are the resolved releases checked._
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: contextgem
3
- Version: 0.25.0
3
+ Version: 0.26.0
4
4
  Summary: Effortless LLM extraction from documents
5
5
  Project-URL: Homepage, https://github.com/shcherbak-ai/contextgem
6
6
  Project-URL: Documentation, https://github.com/shcherbak-ai/contextgem/blob/main/README.md
@@ -10,6 +10,7 @@ Author-email: shcherbak-ai <sergii@shcherbak.ai>
10
10
  Maintainer-email: shcherbak-ai <sergii@shcherbak.ai>
11
11
  License: Apache-2.0
12
12
  License-File: LICENSE
13
+ License-File: LICENSES.md
13
14
  License-File: NOTICE
14
15
  Keywords: artificial-intelligence,aspect-extraction,automated-prompting,concept-extraction,content-extraction,context-aware,contextgem,contract-analysis,contract-automation,contract-intelligence,contract-management,contract-parsing,contract-review,data-extraction,document,document-analysis,document-extraction,document-intelligence,document-parsing,document-pipeline,document-processing,document-qa,document-understanding,entity-extraction,extraction-justifications,extraction-pipeline,fintech,generative-ai,information-extraction,insights-extraction,knowledge-extraction,large-language-models,legaltech,llm,llm-extraction,llm-framework,llm-library,llm-reasoning,low-code,machine-learning,multilingual,multimodal,neural-segmentation,nlp,no-prompt-engineering,prompt-free,question-answering,reference-mapping,semantic-analysis,structured-data,structured-data-extraction,text-analysis,text-processing,topic-extraction,unstructured-data,zero-shot
15
16
  Classifier: Development Status :: 4 - Beta
@@ -41,13 +42,13 @@ Requires-Dist: aiolimiter<2.0.0,>=1.2.1
41
42
  Requires-Dist: colorlog<7.0.0,>=6.9.0
42
43
  Requires-Dist: docstring-parser<1.0.0,>=0.17.0
43
44
  Requires-Dist: fastjsonschema<3.0.0,>=2.21.2
44
- Requires-Dist: genai-prices==0.0.59
45
+ Requires-Dist: genai-prices==0.0.72
45
46
  Requires-Dist: jinja2<4.0.0,>=3.1.5
46
- Requires-Dist: litellm==1.83.14
47
+ Requires-Dist: litellm==1.93.0
47
48
  Requires-Dist: lxml<7.0.0,>=5.4.0
48
49
  Requires-Dist: onnxruntime<1.24.0; python_version < '3.11'
49
50
  Requires-Dist: onnxruntime>=1.24.0; python_version >= '3.14'
50
- Requires-Dist: openai==2.24.0
51
+ Requires-Dist: openai==2.49.0
51
52
  Requires-Dist: pillow<12.0.0,>=11.3.0
52
53
  Requires-Dist: pydantic<3.0.0,>=2.10.6
53
54
  Requires-Dist: python-ulid<4.0.0,>=3.0.0
@@ -64,7 +65,7 @@ Description-Content-Type: text/markdown
64
65
  | | |
65
66
  |----------|--------|
66
67
  | **Package** | [![PyPI](https://img.shields.io/pypi/v/contextgem?logo=pypi&label=PyPi&logoColor=gold&style=flat-square)](https://pypi.org/project/contextgem/) [![PyPI Downloads](https://static.pepy.tech/badge/contextgem/month)](https://pepy.tech/projects/contextgem) [![Python Versions](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-blue?logo=python&logoColor=gold&style=flat-square)](https://www.python.org/downloads/) [![License](https://img.shields.io/badge/License-Apache_2.0-blue?style=flat-square)](https://opensource.org/licenses/Apache-2.0) |
67
- | **Quality** | [![tests](https://img.shields.io/github/actions/workflow/status/shcherbak-ai/contextgem/ci-tests.yml?branch=main&style=flat-square&label=tests)](https://github.com/shcherbak-ai/contextgem/actions/workflows/ci-tests.yml) [![Coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/SergiiShcherbak/daaee00e1dfff7a29ca10a922ec3becd/raw/coverage.json&style=flat-square)](https://github.com/shcherbak-ai/contextgem/actions) [![CodeQL](https://img.shields.io/github/actions/workflow/status/shcherbak-ai/contextgem/codeql.yml?branch=main&style=flat-square&label=CodeQL)](https://github.com/shcherbak-ai/contextgem/actions/workflows/codeql.yml) [![security: bandit](https://img.shields.io/badge/security-bandit-yellow.svg?style=flat-square)](https://github.com/PyCQA/bandit) [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/10489/badge?1)](https://www.bestpractices.dev/projects/10489) |
68
+ | **Quality** | [![tests](https://img.shields.io/github/actions/workflow/status/shcherbak-ai/contextgem/ci-tests.yml?branch=main&style=flat-square&label=tests)](https://github.com/shcherbak-ai/contextgem/actions/workflows/ci-tests.yml) [![Coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/SergiiShcherbak/daaee00e1dfff7a29ca10a922ec3becd/raw/coverage.json&style=flat-square)](https://github.com/shcherbak-ai/contextgem/actions) [![CodeQL](https://img.shields.io/github/actions/workflow/status/shcherbak-ai/contextgem/codeql.yml?branch=main&style=flat-square&label=CodeQL)](https://github.com/shcherbak-ai/contextgem/actions/workflows/codeql.yml) [![license compatibility](https://img.shields.io/github/actions/workflow/status/shcherbak-ai/contextgem/licenseal.yml?branch=main&style=flat-square&label=licenses)](https://github.com/shcherbak-ai/contextgem/actions/workflows/licenseal.yml) [![security: bandit](https://img.shields.io/badge/security-bandit-yellow.svg?style=flat-square)](https://github.com/PyCQA/bandit) [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/10489/badge?1)](https://www.bestpractices.dev/projects/10489) |
68
69
  | **Tools** | [![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json&style=flat-square)](https://github.com/astral-sh/uv) [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json&style=flat-square)](https://github.com/astral-sh/ruff) [![Pydantic v2](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/pydantic/pydantic/main/docs/badge/v2.json&style=flat-square)](https://pydantic.dev) [![ty](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ty/main/assets/badge/v0.json&style=flat-square)](https://github.com/astral-sh/ty) [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-blue?logo=pre-commit&logoColor=white&style=flat-square)](https://github.com/pre-commit/pre-commit) [![deptry](https://img.shields.io/badge/deptry-checked-blue?style=flat-square)](https://github.com/fpgmaas/deptry) [![egress: tethered](https://img.shields.io/badge/egress-tethered-orange?labelColor=4B8BBE)](https://github.com/shcherbak-ai/tethered) [![Hatch project](https://img.shields.io/badge/%F0%9F%A5%9A-Hatch-4051b5.svg?style=flat-square)](https://github.com/pypa/hatch) |
69
70
  | **Docs** | [![docs](https://img.shields.io/github/actions/workflow/status/shcherbak-ai/contextgem/docs.yml?branch=main&style=flat-square&label=docs)](https://github.com/shcherbak-ai/contextgem/actions/workflows/docs.yml) [![documentation](https://img.shields.io/badge/docs-latest-blue?style=flat-square)](https://shcherbak-ai.github.io/contextgem/) ![Docstring Coverage](https://contextgem.dev/_static/interrogate-badge.svg) [![DeepWiki](https://img.shields.io/static/v1?label=DeepWiki&message=Chat%20with%20Code&labelColor=%23283593&color=%237E57C2&style=flat-square)](https://deepwiki.com/shcherbak-ai/contextgem) |
70
71
  | **Community** | [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa?style=flat-square)](CODE_OF_CONDUCT.md) [![GitHub issues closed](https://img.shields.io/github/issues-closed/shcherbak-ai/contextgem?style=flat-square)](https://github.com/shcherbak-ai/contextgem/issues?q=is%3Aissue+is%3Aclosed) [![GitHub latest commit](https://img.shields.io/github/last-commit/shcherbak-ai/contextgem?label=latest%20commit&style=flat-square)](https://github.com/shcherbak-ai/contextgem/commits/main) |
@@ -421,4 +422,5 @@ Built with ❤️ in Oslo, Norway.
421
422
 
422
423
  | Package | Description |
423
424
  |---------|-------------|
425
+ | **[licenseal](https://github.com/shcherbak-ai/licenseal)** [![PyPI](https://img.shields.io/pypi/v/licenseal?logo=pypi&logoColor=gold&style=flat-square)](https://pypi.org/project/licenseal/) [![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-blue.svg?style=flat-square)](https://github.com/shcherbak-ai/licenseal/blob/main/LICENSE) | Fast cross-ecosystem license compatibility checker for CI, audits, and enterprise adoption. Scans manifests, lockfiles, and public registry metadata without installing dependencies. |
424
426
  | **[tethered](https://github.com/shcherbak-ai/tethered)** [![PyPI](https://img.shields.io/pypi/v/tethered?logo=pypi&logoColor=gold&style=flat-square)](https://pypi.org/project/tethered/) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](https://github.com/shcherbak-ai/tethered/blob/main/LICENSE) | Runtime network egress control for Python. One function call blocks all unauthorized outbound connections — zero dependencies, no infrastructure changes. Ideal for supply chain defense, AI agent guardrails, and test isolation. |
@@ -5,7 +5,7 @@
5
5
  | | |
6
6
  |----------|--------|
7
7
  | **Package** | [![PyPI](https://img.shields.io/pypi/v/contextgem?logo=pypi&label=PyPi&logoColor=gold&style=flat-square)](https://pypi.org/project/contextgem/) [![PyPI Downloads](https://static.pepy.tech/badge/contextgem/month)](https://pepy.tech/projects/contextgem) [![Python Versions](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-blue?logo=python&logoColor=gold&style=flat-square)](https://www.python.org/downloads/) [![License](https://img.shields.io/badge/License-Apache_2.0-blue?style=flat-square)](https://opensource.org/licenses/Apache-2.0) |
8
- | **Quality** | [![tests](https://img.shields.io/github/actions/workflow/status/shcherbak-ai/contextgem/ci-tests.yml?branch=main&style=flat-square&label=tests)](https://github.com/shcherbak-ai/contextgem/actions/workflows/ci-tests.yml) [![Coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/SergiiShcherbak/daaee00e1dfff7a29ca10a922ec3becd/raw/coverage.json&style=flat-square)](https://github.com/shcherbak-ai/contextgem/actions) [![CodeQL](https://img.shields.io/github/actions/workflow/status/shcherbak-ai/contextgem/codeql.yml?branch=main&style=flat-square&label=CodeQL)](https://github.com/shcherbak-ai/contextgem/actions/workflows/codeql.yml) [![security: bandit](https://img.shields.io/badge/security-bandit-yellow.svg?style=flat-square)](https://github.com/PyCQA/bandit) [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/10489/badge?1)](https://www.bestpractices.dev/projects/10489) |
8
+ | **Quality** | [![tests](https://img.shields.io/github/actions/workflow/status/shcherbak-ai/contextgem/ci-tests.yml?branch=main&style=flat-square&label=tests)](https://github.com/shcherbak-ai/contextgem/actions/workflows/ci-tests.yml) [![Coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/SergiiShcherbak/daaee00e1dfff7a29ca10a922ec3becd/raw/coverage.json&style=flat-square)](https://github.com/shcherbak-ai/contextgem/actions) [![CodeQL](https://img.shields.io/github/actions/workflow/status/shcherbak-ai/contextgem/codeql.yml?branch=main&style=flat-square&label=CodeQL)](https://github.com/shcherbak-ai/contextgem/actions/workflows/codeql.yml) [![license compatibility](https://img.shields.io/github/actions/workflow/status/shcherbak-ai/contextgem/licenseal.yml?branch=main&style=flat-square&label=licenses)](https://github.com/shcherbak-ai/contextgem/actions/workflows/licenseal.yml) [![security: bandit](https://img.shields.io/badge/security-bandit-yellow.svg?style=flat-square)](https://github.com/PyCQA/bandit) [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/10489/badge?1)](https://www.bestpractices.dev/projects/10489) |
9
9
  | **Tools** | [![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json&style=flat-square)](https://github.com/astral-sh/uv) [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json&style=flat-square)](https://github.com/astral-sh/ruff) [![Pydantic v2](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/pydantic/pydantic/main/docs/badge/v2.json&style=flat-square)](https://pydantic.dev) [![ty](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ty/main/assets/badge/v0.json&style=flat-square)](https://github.com/astral-sh/ty) [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-blue?logo=pre-commit&logoColor=white&style=flat-square)](https://github.com/pre-commit/pre-commit) [![deptry](https://img.shields.io/badge/deptry-checked-blue?style=flat-square)](https://github.com/fpgmaas/deptry) [![egress: tethered](https://img.shields.io/badge/egress-tethered-orange?labelColor=4B8BBE)](https://github.com/shcherbak-ai/tethered) [![Hatch project](https://img.shields.io/badge/%F0%9F%A5%9A-Hatch-4051b5.svg?style=flat-square)](https://github.com/pypa/hatch) |
10
10
  | **Docs** | [![docs](https://img.shields.io/github/actions/workflow/status/shcherbak-ai/contextgem/docs.yml?branch=main&style=flat-square&label=docs)](https://github.com/shcherbak-ai/contextgem/actions/workflows/docs.yml) [![documentation](https://img.shields.io/badge/docs-latest-blue?style=flat-square)](https://shcherbak-ai.github.io/contextgem/) ![Docstring Coverage](https://contextgem.dev/_static/interrogate-badge.svg) [![DeepWiki](https://img.shields.io/static/v1?label=DeepWiki&message=Chat%20with%20Code&labelColor=%23283593&color=%237E57C2&style=flat-square)](https://deepwiki.com/shcherbak-ai/contextgem) |
11
11
  | **Community** | [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa?style=flat-square)](CODE_OF_CONDUCT.md) [![GitHub issues closed](https://img.shields.io/github/issues-closed/shcherbak-ai/contextgem?style=flat-square)](https://github.com/shcherbak-ai/contextgem/issues?q=is%3Aissue+is%3Aclosed) [![GitHub latest commit](https://img.shields.io/github/last-commit/shcherbak-ai/contextgem?label=latest%20commit&style=flat-square)](https://github.com/shcherbak-ai/contextgem/commits/main) |
@@ -362,4 +362,5 @@ Built with ❤️ in Oslo, Norway.
362
362
 
363
363
  | Package | Description |
364
364
  |---------|-------------|
365
+ | **[licenseal](https://github.com/shcherbak-ai/licenseal)** [![PyPI](https://img.shields.io/pypi/v/licenseal?logo=pypi&logoColor=gold&style=flat-square)](https://pypi.org/project/licenseal/) [![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-blue.svg?style=flat-square)](https://github.com/shcherbak-ai/licenseal/blob/main/LICENSE) | Fast cross-ecosystem license compatibility checker for CI, audits, and enterprise adoption. Scans manifests, lockfiles, and public registry metadata without installing dependencies. |
365
366
  | **[tethered](https://github.com/shcherbak-ai/tethered)** [![PyPI](https://img.shields.io/pypi/v/tethered?logo=pypi&logoColor=gold&style=flat-square)](https://pypi.org/project/tethered/) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](https://github.com/shcherbak-ai/tethered/blob/main/LICENSE) | Runtime network egress control for Python. One function call blocks all unauthorized outbound connections — zero dependencies, no infrastructure changes. Ideal for supply chain defense, AI agent guardrails, and test isolation. |
@@ -20,7 +20,7 @@
20
20
  ContextGem - Effortless LLM extraction from documents
21
21
  """
22
22
 
23
- __version__ = "0.25.0"
23
+ __version__ = "0.26.0"
24
24
  __author__ = "Shcherbak AI AS"
25
25
 
26
26
  from contextgem.public import (
@@ -219,6 +219,28 @@ class _Aspect(
219
219
  if aspect.aspects:
220
220
  aspect._validate_and_process_sub_aspects(aspect.aspects) # ty: ignore[invalid-argument-type]
221
221
 
222
+ def _reset_nesting_levels(self, level: int = 0) -> None:
223
+ """
224
+ Recursively (re)assigns nesting levels, treating this aspect as residing
225
+ at ``level`` and its sub-aspects at the subsequent levels.
226
+
227
+ ``_nesting_level`` is assigned relative to the parent when an aspect is
228
+ attached as a sub-aspect, and is never otherwise reset. When an aspect
229
+ that was previously used as a sub-aspect (nesting level > 0) is reused as
230
+ a top-level aspect (e.g. attached to a ``Document``), its stale level
231
+ must be normalized so that nesting-level validation and sub-aspect
232
+ extraction behave correctly. Top-level containers call this with
233
+ ``level=0``.
234
+
235
+ :param level: The nesting level to assign to this aspect.
236
+ :type level: int
237
+ :return: None
238
+ :rtype: None
239
+ """
240
+ self._nesting_level = level
241
+ for sub_aspect in self.aspects:
242
+ sub_aspect._reset_nesting_levels(level + 1)
243
+
222
244
  @model_validator(mode="after")
223
245
  def _validate_aspect_post(self) -> Self:
224
246
  """
@@ -28,7 +28,13 @@ from copy import deepcopy
28
28
  from pathlib import Path
29
29
  from typing import Annotated, Any, Literal
30
30
 
31
- from pydantic import BeforeValidator, Field, field_validator, model_validator
31
+ from pydantic import (
32
+ BeforeValidator,
33
+ Field,
34
+ PrivateAttr,
35
+ field_validator,
36
+ model_validator,
37
+ )
32
38
  from typing_extensions import Self
33
39
 
34
40
  from contextgem.internal.base.aspects import _Aspect
@@ -111,6 +117,12 @@ class _Document(_AssignedInstancesProcessor, _MarkdownTextAttributesProcessor):
111
117
  ),
112
118
  )
113
119
 
120
+ # When True, top-level aspect nesting normalization is suppressed. Set only by
121
+ # the extraction engine on the throwaway single-aspect documents it builds from
122
+ # existing (already-nested) aspects, where the added aspect must keep its
123
+ # nesting level so it still compares equal to the instance being processed.
124
+ _suppress_aspect_nesting_normalization: bool = PrivateAttr(default=False)
125
+
114
126
  def __setattr__(self, name: str, value: Any) -> None:
115
127
  """
116
128
  Sets the attribute of an instance, with additional restrictions on specific attributes.
@@ -161,6 +173,34 @@ class _Document(_AssignedInstancesProcessor, _MarkdownTextAttributesProcessor):
161
173
  self._set_text_from_paras()
162
174
  self._segment_document_text()
163
175
 
176
+ def _normalize_top_level_aspect_nesting(self) -> None:
177
+ """
178
+ Normalizes the nesting levels of the document's top-level aspects to 0
179
+ (propagating to sub-aspects).
180
+
181
+ Aspects attached to a document are top-level. An aspect previously used
182
+ as a sub-aspect carries a non-zero ``_nesting_level``; left unchanged,
183
+ that stale level prevents assigning sub-aspects to it and breaks
184
+ nesting-level validation during extraction. Runs on construction and on
185
+ every aspect (re)assignment via the post-validation hook, so all
186
+ user-facing paths (constructor, ``aspects`` setter, ``add_aspects``,
187
+ ``assign_pipeline``) are covered.
188
+
189
+ Skipped when ``_suppress_aspect_nesting_normalization`` is set, i.e. on
190
+ the throwaway single-aspect documents the extraction engine builds from
191
+ already-nested aspects — those add an existing sub-aspect and rely on it
192
+ comparing equal (including ``_nesting_level``) to the instance being
193
+ processed.
194
+
195
+ :return: None
196
+ :rtype: None
197
+ """
198
+ if self._suppress_aspect_nesting_normalization:
199
+ return
200
+ for aspect in self.aspects:
201
+ if aspect._nesting_level != 0:
202
+ aspect._reset_nesting_levels(0)
203
+
164
204
  def assign_pipeline(
165
205
  self,
166
206
  pipeline: _ExtractionPipeline | _DocumentPipeline,
@@ -479,6 +519,28 @@ class _Document(_AssignedInstancesProcessor, _MarkdownTextAttributesProcessor):
479
519
  :raises ValueError: If paragraphs are not matched or ordered correctly in
480
520
  the document text, or if `_md_text` is provided without `raw_text`.
481
521
  """
522
+ self._run_post_validation()
523
+ return self
524
+
525
+ def _run_post_validation(self) -> None:
526
+ """
527
+ Performs document post-validation: normalizes top-level aspect nesting
528
+ levels and checks paragraph/markdown consistency.
529
+
530
+ Implemented as a plain method (separate from the ``model_validator``
531
+ wrapper) so it can also be invoked during deserialization, once private
532
+ attributes such as ``_md_text`` have been restored.
533
+
534
+ :return: None
535
+ :rtype: None
536
+ :raises ValueError: If paragraphs are not matched or ordered correctly in
537
+ the document text, or if `_md_text` is provided without `raw_text`.
538
+ """
539
+
540
+ # Aspects attached to a document are top-level: normalize their nesting
541
+ # levels (no-op when already correct, or when suppressed for the
542
+ # extraction engine's internal single-aspect documents).
543
+ self._normalize_top_level_aspect_nesting()
482
544
 
483
545
  # Validate that _md_text cannot be populated without raw_text
484
546
  if self._md_text and not self.raw_text:
@@ -516,4 +578,19 @@ class _Document(_AssignedInstancesProcessor, _MarkdownTextAttributesProcessor):
516
578
  self.paragraphs, self._md_text, "markdown"
517
579
  )
518
580
 
519
- return self
581
+ def _revalidate_after_deserialization(self) -> None:
582
+ """
583
+ Re-runs document consistency validation after private attributes have
584
+ been restored during deserialization.
585
+
586
+ ``from_dict`` constructs the instance from public fields (running model
587
+ validators) and only then restores private attributes such as
588
+ ``_md_text``. Because the document's markdown-consistency checks are
589
+ gated on ``_md_text`` being populated, they are skipped at construction
590
+ time during deserialization; this hook re-runs them (and re-normalizes
591
+ aspect nesting levels) once the full state is in place.
592
+
593
+ :return: None
594
+ :rtype: None
595
+ """
596
+ self._run_post_validation()
@@ -136,7 +136,7 @@ if TYPE_CHECKING:
136
136
  )
137
137
 
138
138
 
139
- litellm.suppress_debug_info = True # ty: ignore[invalid-assignment]
139
+ litellm.suppress_debug_info = True
140
140
 
141
141
 
142
142
  # Local model providers supported via liteLLM
@@ -512,6 +512,11 @@ class _GenericLLMProcessor(_AbstractGenericLLMProcessor):
512
512
  _Document,
513
513
  paragraphs=aspect.reference_paragraphs,
514
514
  )
515
+ # Keep the sub-aspects' nesting levels intact: the extraction engine
516
+ # checks "aspect is assigned to document" via instance equality (which
517
+ # includes `_nesting_level`), so the added aspects must stay equal to
518
+ # the instances being processed rather than being normalized to 0.
519
+ aspect_document._suppress_aspect_nesting_normalization = True
515
520
  aspect_document.add_aspects(aspect.aspects) # ty: ignore[invalid-argument-type]
516
521
  sub_aspect_jobs.append((aspect, aspect_document))
517
522
 
@@ -734,6 +739,11 @@ class _GenericLLMProcessor(_AbstractGenericLLMProcessor):
734
739
  _Document,
735
740
  paragraphs=sub_aspect.reference_paragraphs,
736
741
  )
742
+ # Keep the sub-aspect's nesting level intact: the extraction engine
743
+ # checks "aspect is assigned to document" via instance equality
744
+ # (which includes `_nesting_level`), so the added aspect must stay
745
+ # equal to the instance being processed rather than normalized to 0.
746
+ sub_aspect_document._suppress_aspect_nesting_normalization = True
737
747
  sub_aspect_document.add_aspects([sub_aspect])
738
748
  sub_aspect_jobs.append((sub_aspect, sub_aspect_document))
739
749
 
@@ -3147,9 +3157,13 @@ class _DocumentLLM(_GenericLLMProcessor):
3147
3157
  description="Sampling temperature [0..1]; higher values increase randomness.",
3148
3158
  )
3149
3159
  top_p: StrictFloat | None = Field(
3150
- default=0.3,
3160
+ default=None,
3151
3161
  ge=0,
3152
- description="Nucleus sampling [0..1]; alternative to temperature.",
3162
+ description=(
3163
+ "Nucleus sampling [0..1]; alternative to temperature. "
3164
+ "Defaults to None so only temperature is sent — some providers "
3165
+ "(e.g. Claude 4.x) reject requests that set both."
3166
+ ),
3153
3167
  )
3154
3168
  seed: StrictInt | None = Field(
3155
3169
  default=None,
@@ -4064,23 +4078,9 @@ class _DocumentLLM(_GenericLLMProcessor):
4064
4078
  "Fallback LLM must not have the exact same config params as the main LLM."
4065
4079
  )
4066
4080
 
4067
- # "minimal" reasoning effort is supported only for gpt-5 models
4068
- if self.reasoning_effort == "minimal" and not (
4069
- self.model.startswith("azure/gpt-5")
4070
- or self.model.startswith("openai/gpt-5")
4071
- ):
4072
- raise ValueError(
4073
- "`reasoning_effort='minimal'` is supported only for gpt-5 models."
4074
- )
4075
-
4076
- # "xhigh" reasoning effort is supported only for gpt-5.2 models
4077
- if self.reasoning_effort == "xhigh" and not (
4078
- self.model.startswith("azure/gpt-5.2")
4079
- or self.model.startswith("openai/gpt-5.2")
4080
- ):
4081
- raise ValueError(
4082
- "`reasoning_effort='xhigh'` is supported only for gpt-5.2 models."
4083
- )
4081
+ # Note: model-specific support for `reasoning_effort` values is not
4082
+ # validated here; validation is delegated to litellm / the provider API,
4083
+ # which raises an error at request time for unsupported combinations.
4084
4084
 
4085
4085
  # Emit relevant warnings
4086
4086
 
@@ -92,6 +92,18 @@ KEY_RESPONSE_SUCCEEDED_PRIVATE = "_response_succeeded"
92
92
  KEY_MESSAGES_PRIVATE = "_messages"
93
93
 
94
94
 
95
+ # Private attrs whose validation lives on a public property of the same name
96
+ # (without the leading underscore). During deserialization these are assigned
97
+ # through the public property setter so that its validation (e.g. element-type
98
+ # and uniqueness checks) runs, instead of writing the backing attribute directly
99
+ # and silently bypassing those invariants.
100
+ _PRIVATE_ATTR_PUBLIC_PROPERTY: dict[str, str] = {
101
+ KEY_EXTRACTED_ITEMS_PRIVATE: "extracted_items",
102
+ KEY_REFERENCE_PARAGRAPHS_PRIVATE: "reference_paragraphs",
103
+ KEY_REFERENCE_SENTENCES_PRIVATE: "reference_sentences",
104
+ }
105
+
106
+
95
107
  class _InstanceSerializer(BaseModel):
96
108
  """
97
109
  Base class that provides reusable methods for serialization and deserialization of instances.
@@ -516,12 +528,41 @@ class _InstanceSerializer(BaseModel):
516
528
 
517
529
  new_instance = cls(**constructor_kwargs)
518
530
 
519
- # Set private attrs separately
531
+ # Set private attrs separately. Where a private attr is exposed through a
532
+ # public property carrying validation, assign via that property so the
533
+ # validation runs (instead of writing the backing attr directly and
534
+ # bypassing it); otherwise set the private attr directly.
520
535
  for priv_k, priv_v in private_attrs.items():
521
- setattr(new_instance, priv_k, priv_v)
536
+ public_name = _PRIVATE_ATTR_PUBLIC_PROPERTY.get(priv_k)
537
+ if public_name is not None and isinstance(
538
+ getattr(type(new_instance), public_name, None), property
539
+ ):
540
+ setattr(new_instance, public_name, priv_v)
541
+ else:
542
+ setattr(new_instance, priv_k, priv_v)
543
+
544
+ # Re-run any validation that depends on private state restored above
545
+ # (and therefore could not run during construction from public fields).
546
+ new_instance._revalidate_after_deserialization()
522
547
 
523
548
  return new_instance
524
549
 
550
+ def _revalidate_after_deserialization(self) -> None:
551
+ """
552
+ Hook to re-run validation that depends on private attributes restored
553
+ after construction during :meth:`from_dict`.
554
+
555
+ Model validators run when the instance is constructed from its public
556
+ fields, before private attributes are restored, so any invariant gated
557
+ on private state is skipped at that point. Subclasses override this to
558
+ re-validate once the full state is in place. The base implementation is
559
+ a no-op.
560
+
561
+ :return: None
562
+ :rtype: None
563
+ """
564
+ return None
565
+
525
566
  @classmethod
526
567
  def _convert_llm_cost_dict(cls, cost_dict: dict[str, Any]) -> _LLMCost:
527
568
  """
@@ -195,7 +195,7 @@ ANSWER FORMAT (MANDATORY):
195
195
  [
196
196
  {
197
197
  "aspect_id": str,
198
- "paragraphs: [
198
+ "paragraphs": [
199
199
  {
200
200
  "paragraph_id": str,
201
201
  {% if reference_depth == "sentences" %}
@@ -220,7 +220,7 @@ ANSWER FORMAT (MANDATORY):
220
220
  {
221
221
  "aspect_id": str,
222
222
  {% if reference_depth == "sentences" %}
223
- "paragraphs: [
223
+ "paragraphs": [
224
224
  {
225
225
  "paragraph_id": str,
226
226
  "sentences": [
@@ -233,7 +233,7 @@ ANSWER FORMAT (MANDATORY):
233
233
  ...
234
234
  ]
235
235
  {% else %}
236
- "paragraph_ids: [
236
+ "paragraph_ids": [
237
237
  str,
238
238
  ...
239
239
  ]
@@ -77,9 +77,37 @@ _JSON_SCHEMA_TYPE_MAP: dict[type, dict[str, Any]] = {
77
77
  }
78
78
 
79
79
 
80
+ def _json_schema_type_of_value(value: Any) -> str | None:
81
+ """
82
+ Returns the JSON Schema ``type`` name for a concrete ``Literal`` value, or
83
+ ``None`` if the value has no single corresponding JSON Schema scalar type.
84
+
85
+ Booleans are checked before integers because ``bool`` is a subclass of
86
+ ``int``.
87
+
88
+ :param value: The literal value to classify.
89
+ :type value: Any
90
+ :returns: The JSON Schema type name (e.g. ``"integer"``), or ``None``.
91
+ :rtype: str | None
92
+ """
93
+ if isinstance(value, bool):
94
+ return "boolean"
95
+ if isinstance(value, int):
96
+ return "integer"
97
+ if isinstance(value, float):
98
+ return "number"
99
+ if isinstance(value, str):
100
+ return "string"
101
+ if value is None:
102
+ return "null"
103
+ return None
104
+
105
+
80
106
  def _python_type_to_json_schema(
81
107
  type_hint: Any,
82
108
  description: str | None = None,
109
+ *,
110
+ _seen: frozenset[int] = frozenset(),
83
111
  ) -> dict[str, Any]:
84
112
  """
85
113
  Convert a Python type hint to a JSON Schema object.
@@ -98,6 +126,11 @@ def _python_type_to_json_schema(
98
126
  :type type_hint: Any
99
127
  :param description: Optional description to include in the schema.
100
128
  :type description: str | None
129
+ :param _seen: Identities (``id()``) of class/TypedDict types currently being
130
+ expanded on the recursion stack. Used to break cycles in self- or
131
+ mutually-referential types; on revisit a generic ``{"type": "object"}``
132
+ is emitted instead of recursing without bound. Internal use only.
133
+ :type _seen: frozenset[int]
101
134
  :returns: JSON Schema object.
102
135
  :rtype: dict[str, Any]
103
136
  :raises TypeError: If the type hint is not supported.
@@ -126,7 +159,7 @@ def _python_type_to_json_schema(
126
159
  item_type = args[0] if args else Any
127
160
  schema = {
128
161
  "type": "array",
129
- "items": _python_type_to_json_schema(item_type),
162
+ "items": _python_type_to_json_schema(item_type, _seen=_seen),
130
163
  }
131
164
  if description:
132
165
  schema["description"] = description
@@ -140,12 +173,14 @@ def _python_type_to_json_schema(
140
173
  if len(args) == 2 and args[1] is ...:
141
174
  schema = {
142
175
  "type": "array",
143
- "items": _python_type_to_json_schema(args[0]),
176
+ "items": _python_type_to_json_schema(args[0], _seen=_seen),
144
177
  }
145
178
  else:
146
179
  schema = {
147
180
  "type": "array",
148
- "prefixItems": [_python_type_to_json_schema(a) for a in args],
181
+ "prefixItems": [
182
+ _python_type_to_json_schema(a, _seen=_seen) for a in args
183
+ ],
149
184
  "minItems": len(args),
150
185
  "maxItems": len(args),
151
186
  }
@@ -160,7 +195,7 @@ def _python_type_to_json_schema(
160
195
  item_type = args[0] if args else Any
161
196
  schema = {
162
197
  "type": "array",
163
- "items": _python_type_to_json_schema(item_type),
198
+ "items": _python_type_to_json_schema(item_type, _seen=_seen),
164
199
  "uniqueItems": True,
165
200
  }
166
201
  if description:
@@ -173,7 +208,9 @@ def _python_type_to_json_schema(
173
208
  value_type = args[1]
174
209
  schema = {
175
210
  "type": "object",
176
- "additionalProperties": _python_type_to_json_schema(value_type),
211
+ "additionalProperties": _python_type_to_json_schema(
212
+ value_type, _seen=_seen
213
+ ),
177
214
  }
178
215
  else:
179
216
  schema = {"type": "object"}
@@ -193,10 +230,12 @@ def _python_type_to_json_schema(
193
230
  if len(non_none_types) == 1 and has_none:
194
231
  # This is Optional[X] - return schema for X
195
232
  # The "required" handling happens at the parameter level
196
- return _python_type_to_json_schema(non_none_types[0], description)
233
+ return _python_type_to_json_schema(
234
+ non_none_types[0], description, _seen=_seen
235
+ )
197
236
 
198
237
  # Multiple types: use anyOf
199
- schema = {"anyOf": [_python_type_to_json_schema(a) for a in args]}
238
+ schema = {"anyOf": [_python_type_to_json_schema(a, _seen=_seen) for a in args]}
200
239
  if description:
201
240
  schema["description"] = description
202
241
  return schema
@@ -207,14 +246,13 @@ def _python_type_to_json_schema(
207
246
  if not literal_values:
208
247
  raise TypeError("Literal type must have at least one value")
209
248
 
210
- # Determine type from first value
211
- first_val = literal_values[0]
212
- if isinstance(first_val, bool):
213
- schema = {"type": "boolean", "enum": literal_values}
214
- elif isinstance(first_val, int):
215
- schema = {"type": "integer", "enum": literal_values}
216
- elif isinstance(first_val, str):
217
- schema = {"type": "string", "enum": literal_values}
249
+ # Only assign a JSON Schema "type" when every literal value maps to the
250
+ # same scalar type; a mixed-type Literal (e.g. Literal[1, "a"]) would
251
+ # otherwise produce an invalid/mislabeled schema (a string under
252
+ # "type": "integer"), so emit a bare enum in that case.
253
+ value_types = {_json_schema_type_of_value(v) for v in literal_values}
254
+ if len(value_types) == 1 and None not in value_types:
255
+ schema = {"type": value_types.pop(), "enum": literal_values}
218
256
  else:
219
257
  schema = {"enum": literal_values}
220
258
 
@@ -224,15 +262,33 @@ def _python_type_to_json_schema(
224
262
 
225
263
  # Handle TypedDict
226
264
  if is_typeddict(type_hint):
265
+ # Break cycles in self-/mutually-referential TypedDicts
266
+ if id(type_hint) in _seen:
267
+ schema = {"type": "object"}
268
+ if description:
269
+ schema["description"] = description
270
+ return schema
227
271
  # type_hint is guaranteed to be a TypedDict type here
228
- return _typeddict_to_json_schema(type_hint, description) # type: ignore[arg-type]
272
+ return _typeddict_to_json_schema(
273
+ type_hint, # type: ignore[arg-type]
274
+ description,
275
+ _seen | {id(type_hint)},
276
+ )
229
277
 
230
278
  # Handle plain classes that might be dict-like or have annotations
231
279
  if isinstance(type_hint, type):
232
280
  # If it's a basic class without special handling, treat as object
233
281
  if hasattr(type_hint, "__annotations__") and type_hint.__annotations__:
282
+ # Break cycles in self-/mutually-referential classes
283
+ if id(type_hint) in _seen:
284
+ schema = {"type": "object"}
285
+ if description:
286
+ schema["description"] = description
287
+ return schema
234
288
  # Class with annotations - try to convert
235
- return _class_to_json_schema(type_hint, description)
289
+ return _class_to_json_schema(
290
+ type_hint, description, _seen | {id(type_hint)}
291
+ )
236
292
 
237
293
  # Fallback: treat as generic object
238
294
  schema = {"type": "object"}
@@ -251,6 +307,7 @@ def _python_type_to_json_schema(
251
307
  def _typeddict_to_json_schema(
252
308
  td_type: type,
253
309
  description: str | None = None,
310
+ _seen: frozenset[int] = frozenset(),
254
311
  ) -> dict[str, Any]:
255
312
  """
256
313
  Convert a TypedDict to a JSON Schema object.
@@ -259,6 +316,9 @@ def _typeddict_to_json_schema(
259
316
  :type td_type: type
260
317
  :param description: Optional description to include in the schema.
261
318
  :type description: str | None
319
+ :param _seen: Identities of class/TypedDict types currently being expanded,
320
+ used for cycle detection. Internal use only.
321
+ :type _seen: frozenset[int]
262
322
  :returns: JSON Schema object.
263
323
  :rtype: dict[str, Any]
264
324
  """
@@ -269,7 +329,7 @@ def _typeddict_to_json_schema(
269
329
 
270
330
  properties: dict[str, Any] = {}
271
331
  for name, hint in hints.items():
272
- properties[name] = _python_type_to_json_schema(hint)
332
+ properties[name] = _python_type_to_json_schema(hint, _seen=_seen)
273
333
 
274
334
  # Get required keys (TypedDict has __required_keys__ and __optional_keys__)
275
335
  required_keys = getattr(td_type, "__required_keys__", frozenset())
@@ -296,6 +356,7 @@ def _typeddict_to_json_schema(
296
356
  def _class_to_json_schema(
297
357
  cls_type: type,
298
358
  description: str | None = None,
359
+ _seen: frozenset[int] = frozenset(),
299
360
  ) -> dict[str, Any]:
300
361
  """
301
362
  Convert a class with annotations to a JSON Schema object.
@@ -304,6 +365,9 @@ def _class_to_json_schema(
304
365
  :type cls_type: type
305
366
  :param description: Optional description to include in the schema.
306
367
  :type description: str | None
368
+ :param _seen: Identities of class/TypedDict types currently being expanded,
369
+ used for cycle detection. Internal use only.
370
+ :type _seen: frozenset[int]
307
371
  :returns: JSON Schema object.
308
372
  :rtype: dict[str, Any]
309
373
  """
@@ -314,7 +378,7 @@ def _class_to_json_schema(
314
378
 
315
379
  properties: dict[str, Any] = {}
316
380
  for name, hint in hints.items():
317
- properties[name] = _python_type_to_json_schema(hint)
381
+ properties[name] = _python_type_to_json_schema(hint, _seen=_seen)
318
382
 
319
383
  schema: dict[str, Any] = {
320
384
  "type": "object",
@@ -214,23 +214,34 @@ def _parse_type_hint(s: str, i: int = 0) -> tuple[Any, int]:
214
214
 
215
215
  # Parse the literal value (string or other)
216
216
  if i < len(s) and s[i] == '"':
217
- # Parse string literal
218
- i += 1 # skip opening quote
217
+ # Parse a double-quoted string literal, honoring backslash escapes.
218
+ # A backslash escapes the next character (so `\\` -> `\` and
219
+ # `\"` -> `"`); only an unescaped `"` terminates the literal. This
220
+ # is the inverse of `_serialize_literal_value`, which escapes `\`
221
+ # and `"` when emitting a string literal value.
219
222
  start = i
220
- # Find the closing quote, accounting for escaped quotes
223
+ i += 1 # skip opening quote
224
+ chars: list[str] = []
225
+ terminated = False
221
226
  while i < len(s):
222
- if s[i] == '"' and (i == 0 or s[i - 1] != "\\"):
227
+ ch = s[i]
228
+ if ch == "\\" and i + 1 < len(s):
229
+ # Consume the backslash and take the next character literally
230
+ chars.append(s[i + 1])
231
+ i += 2
232
+ continue
233
+ if ch == '"':
234
+ terminated = True
223
235
  break
236
+ chars.append(ch)
224
237
  i += 1
225
238
 
226
- if i >= len(s):
239
+ if not terminated:
227
240
  raise ValueError(
228
241
  f"Unterminated string literal in literal type at position {start} in {s!r}"
229
242
  )
230
243
 
231
- # Get the string value and unescape quotes
232
- string_value = s[start:i].replace('\\"', '"')
233
- values.append(string_value)
244
+ values.append("".join(chars))
234
245
  i += 1 # skip closing quote
235
246
 
236
247
  else:
@@ -91,7 +91,7 @@ DefaultDecimalField = Field(
91
91
  default_factory=lambda: Decimal("0.00000"), ge=Decimal("0.00000")
92
92
  )
93
93
 
94
- ReasoningEffort = Literal["minimal", "low", "medium", "high", "xhigh"]
94
+ ReasoningEffort = Literal["none", "minimal", "low", "medium", "high", "xhigh", "max"]
95
95
 
96
96
  TextMode = Literal["raw", "markdown"]
97
97
 
@@ -49,6 +49,42 @@ PRIMITIVE_TYPES_STRING_MAP = {
49
49
  }
50
50
 
51
51
 
52
+ def _serialize_literal_value(value: Any) -> str:
53
+ """
54
+ Serializes a single ``Literal`` value to its canonical string token.
55
+
56
+ The output is the inverse of the literal-value parsing performed during
57
+ deserialization (``strings_to_types._parse_type_hint``), so that
58
+ ``Literal`` round-trips losslessly:
59
+
60
+ - ``str`` -> a double-quoted string with ``\\`` and ``"`` escaped
61
+ - ``True`` / ``False`` -> ``"true"`` / ``"false"`` (lowercase JSON tokens)
62
+ - ``None`` -> ``"null"``
63
+ - ``int`` / ``float`` -> their plain string representation
64
+
65
+ Booleans are checked before the ``str`` branch is reached and before any
66
+ numeric handling, since ``bool`` is a subclass of ``int``.
67
+
68
+ :param value: The literal value to serialize.
69
+ :type value: Any
70
+ :return: The canonical string token for the literal value.
71
+ :rtype: str
72
+ """
73
+ if isinstance(value, str):
74
+ # Escape backslashes first, then double quotes, and wrap in double quotes.
75
+ # Order matters: escaping quotes first would double-escape the backslashes
76
+ # introduced by the quote escaping.
77
+ escaped = value.replace("\\", "\\\\").replace('"', '\\"')
78
+ return f'"{escaped}"'
79
+ if value is True:
80
+ return "true"
81
+ if value is False:
82
+ return "false"
83
+ if value is None:
84
+ return "null"
85
+ return str(value)
86
+
87
+
52
88
  def _raise_json_serializable_type_error(
53
89
  invalid_type: Any,
54
90
  field_name: str | None = None,
@@ -243,14 +279,7 @@ def _format_type(typ: Any, indent_level: int = 0) -> str:
243
279
  if getattr(arg, "__origin__", None) is Literal:
244
280
  # Extract individual literal values
245
281
  for literal_arg in get_args(arg):
246
- if isinstance(literal_arg, str):
247
- flattened_values.append(
248
- '"' + literal_arg.replace('"', '\\"') + '"'
249
- )
250
- elif literal_arg is None:
251
- flattened_values.append("null")
252
- else:
253
- flattened_values.append(str(literal_arg))
282
+ flattened_values.append(_serialize_literal_value(literal_arg))
254
283
  elif arg is type(None):
255
284
  flattened_values.append("null")
256
285
  else:
@@ -268,17 +297,7 @@ def _format_type(typ: Any, indent_level: int = 0) -> str:
268
297
  # Handle Literal types
269
298
  if getattr(typ, "__origin__", None) is Literal:
270
299
  # Create a list of serialized literal values
271
- literal_values = []
272
- for arg in get_args(typ):
273
- if isinstance(arg, str):
274
- # Escape quotes in strings and wrap in quotes
275
- literal_values.append('"' + arg.replace('"', '\\"') + '"')
276
- elif arg is None:
277
- # Handle None consistently with standalone None type
278
- literal_values.append("null")
279
- else:
280
- # For non-string literals, just convert to string
281
- literal_values.append(str(arg))
300
+ literal_values = [_serialize_literal_value(arg) for arg in get_args(typ)]
282
301
 
283
302
  # Join all literal values with "or" instead of commas
284
303
  values_str = " or ".join(literal_values)
@@ -468,14 +487,7 @@ def _serialize_type_hint(tp: Any) -> str:
468
487
  # Handle Literal types
469
488
  elif origin is Literal:
470
489
  # Create a list of serialized literal values
471
- literal_values = []
472
- for arg in args:
473
- if isinstance(arg, str):
474
- # Escape quotes in strings and wrap in quotes
475
- literal_values.append('"' + arg.replace('"', '\\"') + '"')
476
- else:
477
- # For non-string literals, just convert to string
478
- literal_values.append(str(arg))
490
+ literal_values = [_serialize_literal_value(arg) for arg in args]
479
491
 
480
492
  # Join all literal values with commas
481
493
  values_str = ", ".join(literal_values)
@@ -103,12 +103,17 @@ class DocumentLLM(_DocumentLLM):
103
103
  (CoT-capable) models. Defaults to 16000.
104
104
  :vartype max_completion_tokens: int
105
105
  :ivar reasoning_effort: The effort level for the LLM to reason about the input. Can be set to
106
- ``"minimal"`` (gpt-5 models only), ``"low"``, ``"medium"``, ``"high"``, or ``"xhigh"``
107
- (gpt-5.2 models only). Relevant for reasoning (CoT-capable) models. Defaults to None.
106
+ ``"none"``, ``"minimal"``, ``"low"``, ``"medium"``, ``"high"``, ``"xhigh"``, or
107
+ ``"max"``. Support for specific values varies by model and provider; unsupported
108
+ combinations are rejected by litellm or the provider API at request time. Note that
109
+ ``"none"`` explicitly disables reasoning on models that support this value, which
110
+ differs from the default ``None`` (parameter not sent, provider default applies).
111
+ Relevant for reasoning (CoT-capable) models. Defaults to None.
108
112
  :vartype reasoning_effort: ReasoningEffort | None
109
113
  :ivar top_p: Nucleus sampling value (0.0 to 1.0) controlling output focus/randomness.
110
114
  Lower values make output more deterministic, higher values produce more diverse outputs.
111
- Defaults to 0.3.
115
+ Defaults to ``None`` (only ``temperature`` is sent). Some providers (e.g. Claude 4.x)
116
+ reject requests that set both ``temperature`` and ``top_p``.
112
117
  :vartype top_p: float | None
113
118
  :ivar num_retries_failed_request: Number of retries when LLM request fails. Defaults to 3.
114
119
  :vartype num_retries_failed_request: int
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "contextgem"
3
- version = "0.25.0"
3
+ version = "0.26.0"
4
4
  description = "Effortless LLM extraction from documents"
5
5
  authors = [{ name = "shcherbak-ai", email = "sergii@shcherbak.ai" }]
6
6
  requires-python = ">=3.10,<3.15"
@@ -96,11 +96,11 @@ dependencies = [
96
96
  "colorlog (>=6.9.0,<7.0.0)",
97
97
  "docstring-parser (>=0.17.0,<1.0.0)",
98
98
  "fastjsonschema (>=2.21.2,<3.0.0)",
99
- "genai-prices==0.0.59", # pin version due to frequent API changes
99
+ "genai-prices==0.0.72", # pin version due to frequent API changes
100
100
  "jinja2 (>=3.1.5,<4.0.0)",
101
- "litellm==1.83.14", # pin version due to occasional instability between patches and minor versions
101
+ "litellm==1.93.0", # pin version due to occasional instability between patches and minor versions
102
102
  "lxml (>=5.4.0,<7.0.0)",
103
- "openai==2.24.0", # pinned version for stable compatibility with litellm (after issue with 1.100.0)
103
+ "openai==2.49.0", # pinned version for stable compatibility with litellm (after issue with 1.100.0)
104
104
  "pillow (>=11.3.0,<12.0.0)",
105
105
  "pydantic (>=2.10.6,<3.0.0)",
106
106
  "python-ulid (>=3.0.0,<4.0.0)",
@@ -132,12 +132,12 @@ dev = [
132
132
  "nest-asyncio (>=1.6.0,<2.0.0)",
133
133
  "pre-commit (>=4.1.0,<5.0.0)",
134
134
  "pympler (>=1.1.0,<2.0.0)",
135
- "ty (>=0.0.34,<1.0.0)",
135
+ "ty (==0.0.35)", # pinned to the last fully-clean ty release; newer ones surface issues here; unpin when issues are fixed
136
136
  "pytest (>=8.3.4,<9.0.0)",
137
137
  "pytest-cov (>=6.0.0,<7.0.0)",
138
138
  "pytest-recording (>=0.13.4,<1.0.0)",
139
139
  "python-dotenv (>=1.0.1,<2.0.0)",
140
- "ruff (>=0.12.3,<1.0.0)",
140
+ "ruff (>=0.12.3,<0.16.0)", # ruff 0.16 expands the default lint rule set, flagging many new issues; capped until a dedicated lint migration
141
141
  "sphinx (>=7.0.0,<8.0.0)",
142
142
  "sphinx-autodoc-typehints<3.0.0",
143
143
  "sphinx-autobuild (>=2024.10.3,<2027.0.0)",
@@ -148,6 +148,7 @@ dev = [
148
148
  "sphinxext-opengraph (>=0.9.1,<1.0.0)",
149
149
  "types-lxml (>=2025.3.30,<2027.0.0)",
150
150
  "twine>=6.2.0",
151
+ "licenseal>=0.1.1",
151
152
  ]
152
153
 
153
154
  [build-system]
@@ -211,7 +212,7 @@ log_cli_level = "WARNING"
211
212
 
212
213
  [tool.commitizen]
213
214
  name = "cz_conventional_commits"
214
- version = "0.25.0"
215
+ version = "0.26.0"
215
216
  version_files = [
216
217
  "contextgem/__init__.py:__version__",
217
218
  "pyproject.toml:version",
File without changes
File without changes