clams-python 1.7.0__tar.gz → 1.7.2__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 (102) hide show
  1. clams_python-1.7.2/.github/workflows/release-check.yml +11 -0
  2. {clams_python-1.7.0 → clams_python-1.7.2}/CHANGELOG.md +21 -0
  3. {clams_python-1.7.0 → clams_python-1.7.2}/CONTRIBUTING.md +7 -0
  4. {clams_python-1.7.0/clams_python.egg-info → clams_python-1.7.2}/PKG-INFO +2 -2
  5. clams_python-1.7.2/build-tools/prep_release.py +104 -0
  6. {clams_python-1.7.0 → clams_python-1.7.2}/clams/app/__init__.py +134 -15
  7. {clams_python-1.7.0 → clams_python-1.7.2/clams_python.egg-info}/PKG-INFO +2 -2
  8. {clams_python-1.7.0 → clams_python-1.7.2}/clams_python.egg-info/SOURCES.txt +5 -0
  9. {clams_python-1.7.0 → clams_python-1.7.2}/clams_python.egg-info/requires.txt +1 -1
  10. clams_python-1.7.2/clams_python.egg-info/scm_file_list.json +96 -0
  11. clams_python-1.7.2/clams_python.egg-info/scm_version.json +8 -0
  12. clams_python-1.7.2/documentation/autodoc/clams.backends.rst +15 -0
  13. {clams_python-1.7.0 → clams_python-1.7.2}/documentation/conf.py +0 -20
  14. {clams_python-1.7.0 → clams_python-1.7.2}/documentation/target-versions.csv +7 -1
  15. {clams_python-1.7.0 → clams_python-1.7.2}/pyproject.toml +1 -1
  16. {clams_python-1.7.0 → clams_python-1.7.2}/tests/test_promptable.py +134 -14
  17. {clams_python-1.7.0 → clams_python-1.7.2}/.github/workflows/codecov.yml +0 -0
  18. {clams_python-1.7.0 → clams_python-1.7.2}/.github/workflows/container.yml +0 -0
  19. {clams_python-1.7.0 → clams_python-1.7.2}/.github/workflows/containers.yml +0 -0
  20. {clams_python-1.7.0 → clams_python-1.7.2}/.github/workflows/issue-assign.yml +0 -0
  21. {clams_python-1.7.0 → clams_python-1.7.2}/.github/workflows/issue-close.yml +0 -0
  22. {clams_python-1.7.0 → clams_python-1.7.2}/.github/workflows/issue-infra-project.yml +0 -0
  23. {clams_python-1.7.0 → clams_python-1.7.2}/.github/workflows/publish.yml +0 -0
  24. {clams_python-1.7.0 → clams_python-1.7.2}/.gitignore +0 -0
  25. {clams_python-1.7.0 → clams_python-1.7.2}/.pytype.cfg +0 -0
  26. {clams_python-1.7.0 → clams_python-1.7.2}/LICENSE +0 -0
  27. {clams_python-1.7.0 → clams_python-1.7.2}/README.md +0 -0
  28. {clams_python-1.7.0 → clams_python-1.7.2}/build-tools/build.py +0 -0
  29. {clams_python-1.7.0 → clams_python-1.7.2}/build-tools/clean.py +0 -0
  30. {clams_python-1.7.0 → clams_python-1.7.2}/build-tools/docs.py +0 -0
  31. {clams_python-1.7.0 → clams_python-1.7.2}/build-tools/publish.py +0 -0
  32. {clams_python-1.7.0 → clams_python-1.7.2}/build-tools/test.py +0 -0
  33. {clams_python-1.7.0 → clams_python-1.7.2}/clams/__init__.py +0 -0
  34. {clams_python-1.7.0 → clams_python-1.7.2}/clams/appmetadata/__init__.py +0 -0
  35. {clams_python-1.7.0 → clams_python-1.7.2}/clams/backends/__init__.py +0 -0
  36. {clams_python-1.7.0 → clams_python-1.7.2}/clams/backends/hf.py +0 -0
  37. {clams_python-1.7.0 → clams_python-1.7.2}/clams/develop/__init__.py +0 -0
  38. {clams_python-1.7.0 → clams_python-1.7.2}/clams/develop/templates/app/.dockerignore.template +0 -0
  39. {clams_python-1.7.0 → clams_python-1.7.2}/clams/develop/templates/app/.gitignore.template +0 -0
  40. {clams_python-1.7.0 → clams_python-1.7.2}/clams/develop/templates/app/Containerfile.template +0 -0
  41. {clams_python-1.7.0 → clams_python-1.7.2}/clams/develop/templates/app/LICENSE.template +0 -0
  42. {clams_python-1.7.0 → clams_python-1.7.2}/clams/develop/templates/app/README.md.template +0 -0
  43. {clams_python-1.7.0 → clams_python-1.7.2}/clams/develop/templates/app/app.py.template +0 -0
  44. {clams_python-1.7.0 → clams_python-1.7.2}/clams/develop/templates/app/cli.py.template +0 -0
  45. {clams_python-1.7.0 → clams_python-1.7.2}/clams/develop/templates/app/metadata.py.template +0 -0
  46. {clams_python-1.7.0 → clams_python-1.7.2}/clams/develop/templates/app/requirements.txt.template +0 -0
  47. {clams_python-1.7.0 → clams_python-1.7.2}/clams/develop/templates/gha/for-clams-team.md.template +0 -0
  48. {clams_python-1.7.0 → clams_python-1.7.2}/clams/develop/templates/gha/workflows/issue-apps-project.yml.template +0 -0
  49. {clams_python-1.7.0 → clams_python-1.7.2}/clams/develop/templates/gha/workflows/issue-assign.yml.template +0 -0
  50. {clams_python-1.7.0 → clams_python-1.7.2}/clams/develop/templates/gha/workflows/issue-close.yml.template +0 -0
  51. {clams_python-1.7.0 → clams_python-1.7.2}/clams/develop/templates/gha/workflows/publish.yml.template +0 -0
  52. {clams_python-1.7.0 → clams_python-1.7.2}/clams/develop/templates/utl-tf/__init__.py.template +0 -0
  53. {clams_python-1.7.0 → clams_python-1.7.2}/clams/develop/templates/utl-tf/timeframe.py.template +0 -0
  54. {clams_python-1.7.0 → clams_python-1.7.2}/clams/envelop/__init__.py +0 -0
  55. {clams_python-1.7.0 → clams_python-1.7.2}/clams/restify/__init__.py +0 -0
  56. {clams_python-1.7.0 → clams_python-1.7.2}/clams/serve/__init__.py +0 -0
  57. {clams_python-1.7.0 → clams_python-1.7.2}/clams/ver/__init__.py +0 -0
  58. {clams_python-1.7.0 → clams_python-1.7.2}/clams_python.egg-info/dependency_links.txt +0 -0
  59. {clams_python-1.7.0 → clams_python-1.7.2}/clams_python.egg-info/entry_points.txt +0 -0
  60. {clams_python-1.7.0 → clams_python-1.7.2}/clams_python.egg-info/top_level.txt +0 -0
  61. {clams_python-1.7.0 → clams_python-1.7.2}/container/Containerfile +0 -0
  62. {clams_python-1.7.0 → clams_python-1.7.2}/container/ffmpeg-hf.containerfile +0 -0
  63. {clams_python-1.7.0 → clams_python-1.7.2}/container/ffmpeg-hf4.containerfile +0 -0
  64. {clams_python-1.7.0 → clams_python-1.7.2}/container/ffmpeg-hf5.containerfile +0 -0
  65. {clams_python-1.7.0 → clams_python-1.7.2}/container/ffmpeg-tf2.containerfile +0 -0
  66. {clams_python-1.7.0 → clams_python-1.7.2}/container/ffmpeg-torch2.containerfile +0 -0
  67. {clams_python-1.7.0 → clams_python-1.7.2}/container/ffmpeg.containerfile +0 -0
  68. {clams_python-1.7.0 → clams_python-1.7.2}/container/generate_containers_yml.py +0 -0
  69. {clams_python-1.7.0 → clams_python-1.7.2}/container/hf.containerfile +0 -0
  70. {clams_python-1.7.0 → clams_python-1.7.2}/container/hf4.containerfile +0 -0
  71. {clams_python-1.7.0 → clams_python-1.7.2}/container/hf5.containerfile +0 -0
  72. {clams_python-1.7.0 → clams_python-1.7.2}/container/jdk8.containerfile +0 -0
  73. {clams_python-1.7.0 → clams_python-1.7.2}/container/opencv4-hf.containerfile +0 -0
  74. {clams_python-1.7.0 → clams_python-1.7.2}/container/opencv4-hf4.containerfile +0 -0
  75. {clams_python-1.7.0 → clams_python-1.7.2}/container/opencv4-hf5.containerfile +0 -0
  76. {clams_python-1.7.0 → clams_python-1.7.2}/container/opencv4-tf2.containerfile +0 -0
  77. {clams_python-1.7.0 → clams_python-1.7.2}/container/opencv4-torch2.containerfile +0 -0
  78. {clams_python-1.7.0 → clams_python-1.7.2}/container/opencv4.containerfile +0 -0
  79. {clams_python-1.7.0 → clams_python-1.7.2}/container/tf2.containerfile +0 -0
  80. {clams_python-1.7.0 → clams_python-1.7.2}/container/torch2.containerfile +0 -0
  81. {clams_python-1.7.0 → clams_python-1.7.2}/documentation/app-baseclasses.rst +0 -0
  82. {clams_python-1.7.0 → clams_python-1.7.2}/documentation/appdirectory.rst +0 -0
  83. {clams_python-1.7.0 → clams_python-1.7.2}/documentation/appmetadata.rst +0 -0
  84. {clams_python-1.7.0 → clams_python-1.7.2}/documentation/autodoc/clams.app.rst +0 -0
  85. {clams_python-1.7.0 → clams_python-1.7.2}/documentation/autodoc/clams.appmetadata.rst +0 -0
  86. {clams_python-1.7.0 → clams_python-1.7.2}/documentation/autodoc/clams.mmif_utils.rst +0 -0
  87. {clams_python-1.7.0 → clams_python-1.7.2}/documentation/autodoc/clams.restify.rst +0 -0
  88. {clams_python-1.7.0 → clams_python-1.7.2}/documentation/clamsapp.rst +0 -0
  89. {clams_python-1.7.0 → clams_python-1.7.2}/documentation/cli.rst +0 -0
  90. {clams_python-1.7.0 → clams_python-1.7.2}/documentation/gpu-apps.rst +0 -0
  91. {clams_python-1.7.0 → clams_python-1.7.2}/documentation/index.rst +0 -0
  92. {clams_python-1.7.0 → clams_python-1.7.2}/documentation/input-output.rst +0 -0
  93. {clams_python-1.7.0 → clams_python-1.7.2}/documentation/introduction.rst +0 -0
  94. {clams_python-1.7.0 → clams_python-1.7.2}/documentation/modules.rst +0 -0
  95. {clams_python-1.7.0 → clams_python-1.7.2}/documentation/runtime-params.rst +0 -0
  96. {clams_python-1.7.0 → clams_python-1.7.2}/documentation/tutorial.md +0 -0
  97. {clams_python-1.7.0 → clams_python-1.7.2}/setup.cfg +0 -0
  98. {clams_python-1.7.0 → clams_python-1.7.2}/tests/metadata.py +0 -0
  99. {clams_python-1.7.0 → clams_python-1.7.2}/tests/pillow-logo.png +0 -0
  100. {clams_python-1.7.0 → clams_python-1.7.2}/tests/test_backends_hf.py +0 -0
  101. {clams_python-1.7.0 → clams_python-1.7.2}/tests/test_clamsapp.py +0 -0
  102. {clams_python-1.7.0 → clams_python-1.7.2}/tests/test_envelope.py +0 -0
@@ -0,0 +1,11 @@
1
+ name: "🔖 Release PR check"
2
+
3
+ on:
4
+ pull_request:
5
+ branches: [main]
6
+ types: [opened, synchronize, reopened, edited]
7
+
8
+ jobs:
9
+ release-check:
10
+ name: "🤙 Call SDK release-check workflow"
11
+ uses: clamsproject/.github/.github/workflows/sdk-release-check.yml@main
@@ -1,4 +1,25 @@
1
1
 
2
+ ## releasing 1.7.1 (2026-06-19)
3
+ ### Overview
4
+ Updates the `mmif-python` SDK dependency to 1.5.1 to incorporate a bugfix in VDH, with some other minor documentation generation improvements.
5
+
6
+
7
+ ## releasing 1.7.0 (2026-05-29)
8
+ ### Overview
9
+ This release introduces a specialized base class `ClamsPromptableApp` and its HuggingFace specialization `ClamsHFPromptableApp` for CLAMS apps that wrap LLM / VLM / ALM / LMM-based analyzers, along with other HF-related helper code in various places. Additionally, it adds `appTags` field for app categorization in app metadata, and new timeframe related local utility recipe for `clams develop --recipe utl-tf` scaffold.
10
+
11
+ ### Additions
12
+ * `ClamsPromptableApp`, a new specialized base class (subclassing `ClamsApp`) for prompt-driven, instruction-tuned LLM apps with pre-equipped with commonly used runtime parameter set (`prompt`, `systemPrompt`, `promptMode`, `maxNewTokens`, `temperature`, `topP`, `topK`, `parallelPrompts`). Plus, `ClamsHFPromptableApp` (subclassing `ClamsPromptableApp`) for the common case of "promptable app + local HuggingFace `transformers` model". This subclass should significanly simplify wrapping HF models into a CLAMS app. See https://clams.ai/clams-python/app-baseclasses.html for the developer guide. (https://github.com/clamsproject/clams-python/pull/291)
13
+ * `clams.backends.hf` (new module) with two general HF loaders: `load_hf_model` (`from_pretrained` flow) and `load_hf_pipeline` (`transformers.pipeline` flow). Wraps the device auto-detection, revision pinning, dtype handling, and kwargs pass-through that every HF-backed CLAMS app does identically; used internally by `ClamsHFPromptableApp` and callable directly from any non-promptable HF app. Requires the `[hf]` extra (`pip install clams-python[hf]`) to keep `torch` and `transformers` out of the base install.
14
+ * `clams develop --recipe utl-tf` scaffold drops a `utils/timeframe.py` into the new app that factors out the canonical TimeFrame iteration / `vdh.extract_images_by_mode_with_sources` / TimePoint-on-fallback / task-tuple pattern. (7bc91d97ff0af2a2253ca24b00afa8b554ad5849)
15
+ * `appTags` field on `AppMetadata`, a sibling to https://github.com/clamsproject/mmif/pull/253. Allows categorization of CLAMS apps for app-directory search / filtering. (https://github.com/clamsproject/clams-python/pull/290)
16
+
17
+ ### Changes
18
+ * Updated to the latest `mmif-python` SDK (1.5.0).
19
+ * Tightened the semantics of the `AppMetadata.analyzer_versions` field (introduced in 1.4.0) for CHFPApp-based development. When an app subclasses `ClamsHFPromptableApp`, the dict is now the source of truth for the family of supported HF model IDs and their pinned commit hashes: the SDK auto-derives a `model` runtime parameter from the dict's keys, and the HF base class refines `model=<id>` into `model=<id>@<revision>` on output so the resolved revision lands in `view.metadata.appConfiguration` automatically.
20
+ * `clams develop` scaffold templates (`app.py.template`, `metadata.py.template`) updated with commented-out blocks for both promptable variants.
21
+
22
+
2
23
  ## releasing 1.6.0 (2026-05-19)
3
24
  ### Overview
4
25
 
@@ -75,6 +75,13 @@ from clams.ver import __version__
75
75
  For a dev install without a matching tag, `setuptools-scm` generates a
76
76
  version like `1.4.1.dev20+gaf551a4e4.d20260325`.
77
77
 
78
+ ## Releases
79
+
80
+ Release PRs (from `develop` to `main`) are gated by the `release-check` CI workflow. Before opening one:
81
+
82
+ 1. Title the PR exactly `releasing X.Y.Z` (strict semver); `release-check` reads the version from the title.
83
+ 2. Run `python build-tools/prep_release.py X.Y.Z` and commit its changes. See inside the prep script to see what's actually prepared.
84
+
78
85
  ## Migration from Makefile
79
86
 
80
87
  The old `Makefile` and `setup.py` have been removed. If you are
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clams-python
3
- Version: 1.7.0
3
+ Version: 1.7.2
4
4
  Summary: A collection of APIs to develop CLAMS app for python
5
5
  License-Expression: Apache-2.0
6
6
  Project-URL: homepage, https://clams.ai
@@ -13,7 +13,7 @@ Classifier: Programming Language :: Python :: 3 :: Only
13
13
  Requires-Python: >=3.10
14
14
  Description-Content-Type: text/markdown
15
15
  License-File: LICENSE
16
- Requires-Dist: mmif-python==1.5.0
16
+ Requires-Dist: mmif-python==1.5.1
17
17
  Requires-Dist: Flask>=2
18
18
  Requires-Dist: Flask-RESTful>=0.3.9
19
19
  Requires-Dist: gunicorn>=20
@@ -0,0 +1,104 @@
1
+ """
2
+ Prepare release artifacts for the clams-python package.
3
+
4
+ Run while preparing a release PR, passing the version being released::
5
+
6
+ python build-tools/prep_release.py X.Y.Z
7
+
8
+ Currently this adds a row to ``documentation/target-versions.csv`` for
9
+ the release version. The ``mmif-python`` column is read from this
10
+ package's pinned dependency in ``pyproject.toml``, and the target MMIF
11
+ spec version from the installed ``mmif.__specver__``. This script is the
12
+ single place that writes the target-versions table; the docs build only
13
+ renders it, and CI verifies (on the release PR) that a row for the PR's
14
+ version exists.
15
+
16
+ Requires an editable install (``pip install -e .``) so that
17
+ ``mmif.__specver__`` is importable.
18
+ """
19
+ import argparse
20
+ import re
21
+ import sys
22
+ from pathlib import Path
23
+
24
+ PROJECT_ROOT = Path(__file__).parent.parent
25
+ CSV_PATH = PROJECT_ROOT / 'documentation' / 'target-versions.csv'
26
+ PYPROJECT_PATH = PROJECT_ROOT / 'pyproject.toml'
27
+
28
+ VERSION_RE = re.compile(r'^\d+\.\d+\.\d+$')
29
+ TOP_ROW_RE = re.compile(r'`(\d+\.\d+\.\d+) <')
30
+ MMIF_PIN_RE = re.compile(r'mmif-python\s*==\s*(\d+\.\d+\.\d+)')
31
+
32
+
33
+ def _row(version, mmif_ver, specver):
34
+ return (
35
+ f'`{version} <https://pypi.org/project/clams-python/{version}/>`__,'
36
+ f'`{mmif_ver} <https://pypi.org/project/mmif-python/{mmif_ver}/>`__,'
37
+ f'`{specver} <https://mmif.clams.ai/{specver}/>`__'
38
+ )
39
+
40
+
41
+ def _version_tuple(v):
42
+ return tuple(int(x) for x in v.split('.'))
43
+
44
+
45
+ def _top_version(data_lines):
46
+ if not data_lines:
47
+ return None
48
+ m = TOP_ROW_RE.match(data_lines[0])
49
+ return m.group(1) if m else None
50
+
51
+
52
+ def _pinned_mmif_version():
53
+ m = MMIF_PIN_RE.search(PYPROJECT_PATH.read_text())
54
+ if not m:
55
+ sys.exit("Error: could not find a pinned 'mmif-python==X.Y.Z' "
56
+ "dependency in pyproject.toml.")
57
+ return m.group(1)
58
+
59
+
60
+ def main():
61
+ parser = argparse.ArgumentParser(
62
+ description="Prepare release artifacts for clams-python."
63
+ )
64
+ parser.add_argument(
65
+ "version", metavar="X.Y.Z",
66
+ help="the release version being prepared",
67
+ )
68
+ args = parser.parse_args()
69
+ version = args.version
70
+
71
+ if not VERSION_RE.match(version):
72
+ sys.exit(f"Error: '{version}' is not a valid X.Y.Z version.")
73
+
74
+ mmif_ver = _pinned_mmif_version()
75
+ try:
76
+ import mmif
77
+ specver = mmif.__specver__
78
+ except (ImportError, AttributeError):
79
+ sys.exit("Error: cannot read mmif.__specver__. "
80
+ "Run `pip install -e .` first.")
81
+
82
+ lines = CSV_PATH.read_text().splitlines()
83
+ header, data = lines[0], lines[1:]
84
+ top = _top_version(data)
85
+
86
+ if top is not None and _version_tuple(version) < _version_tuple(top):
87
+ sys.exit(f"Error: {version} is older than the current top row "
88
+ f"{top}; refusing to insert an out-of-order version.")
89
+
90
+ new_row = _row(version, mmif_ver, specver)
91
+ if top == version:
92
+ data[0] = new_row # update in place
93
+ action = "updated"
94
+ else:
95
+ data.insert(0, new_row) # prepend as the new latest
96
+ action = "added"
97
+
98
+ CSV_PATH.write_text('\n'.join([header] + data) + '\n')
99
+ print(f"target-versions.csv: {action} row {version} "
100
+ f"-> mmif-python {mmif_ver}, MMIF spec {specver}")
101
+
102
+
103
+ if __name__ == "__main__":
104
+ main()
@@ -1005,8 +1005,10 @@ class ClamsPromptableApp(ClamsApp):
1005
1005
  """
1006
1006
  Persist a single LLM text response into a view. Writes one
1007
1007
  ``TextDocument`` (containing the response) plus possible
1008
- grounding via an ``Alignment`` annotation and ``origins`` /
1009
- ``origination`` properties on the TD.
1008
+ grounding via an ``Alignment`` annotation and ``origins`` /
1009
+ ``origination`` properties on the TD. The TD also inherits the
1010
+ ``document`` of its ``source`` annotation, so it records which
1011
+ medium the response describes.
1010
1012
 
1011
1013
  The two grounding link kinds are semantically distinct:
1012
1014
 
@@ -1028,7 +1030,12 @@ class ClamsPromptableApp(ClamsApp):
1028
1030
  ``Alignment`` first if needed.
1029
1031
  :param source: ``id`` of the annotation to record as the
1030
1032
  cross-modal anchor of the response (see above).
1031
- :param response: the text generated by the model.
1033
+ :param response: the text to store as the TextDocument's
1034
+ value. Typically the model's generated answer, but the
1035
+ caller MAY pass a post-processed form (e.g. with a
1036
+ reasoning block stripped, whitespace trimmed, or otherwise
1037
+ reformatted); this method stores whatever it is given,
1038
+ verbatim, as the ``text``.
1032
1039
  :param origins: optional list of ``id``\\s of annotations the
1033
1040
  response was *derived* from. Must be paired with
1034
1041
  ``origination``.
@@ -1039,15 +1046,24 @@ class ClamsPromptableApp(ClamsApp):
1039
1046
  ``origins``.
1040
1047
  :param reasoning_trace: optional model-side reasoning trace
1041
1048
  (a chain-of-thought / scratchpad string, NOT a Python
1042
- traceback). NOT YET SUPPORTED -- passing a non-``None``
1043
- value raises :py:class:`NotImplementedError`. Storage
1044
- convention is still being decided at
1045
- clamsproject/clams-python#263.
1049
+ traceback). When non-``None``, it is stored on the
1050
+ returned TextDocument as the ``modelReasoningTrace``
1051
+ property, kept OUT of the ``text`` (which holds whatever
1052
+ ``response`` the caller passed). The point is separation,
1053
+ not any claim about the ``text`` being verbatim: storing a
1054
+ trace adds no annotation and no new type, so the type-level
1055
+ output shape is identical whether or not a trace is stored.
1056
+ Pass ``None`` (the default) to store no trace. The property
1057
+ name is provisional pending the
1058
+ vocabulary convention (clamsproject/clams-vocabulary#11).
1046
1059
  :return: ``(TextDocument, Alignment)`` tuple of the new
1047
1060
  annotations.
1048
1061
  :raises ValueError: if exactly one of ``origins`` /
1049
- ``origination`` is set; they must be supplied together
1050
- or both omitted.
1062
+ ``origination`` is set (they must be supplied together or
1063
+ both omitted), or if the ``source`` annotation carries no
1064
+ ``document`` to ground the response to (a malformed input).
1065
+ :raises KeyError: if ``source`` does not resolve to an
1066
+ annotation in the MMIF.
1051
1067
  """
1052
1068
  if bool(origins) != bool(origination):
1053
1069
  raise ValueError(
@@ -1056,6 +1072,20 @@ class ClamsPromptableApp(ClamsApp):
1056
1072
  f"origins={origins!r}, origination={origination!r}."
1057
1073
  )
1058
1074
  td = view.new_textdocument(text=response)
1075
+ # Ground the TextDocument to the medium it describes: copy `document`
1076
+ # from the source annotation (e.g. the parent TimeFrame's video) onto
1077
+ # the TD, so it records which document the response pertains to. A
1078
+ # grounded response must be traceable to a document, so a source that
1079
+ # carries no `document` means the upstream MMIF is malformed -- raise
1080
+ # rather than silently drop the grounding.
1081
+ source_ann = view._parent_mmif[source]
1082
+ source_doc = source_ann.get_property('document')
1083
+ if not source_doc:
1084
+ raise ValueError(
1085
+ f"source annotation {source!r} has no `document` property to "
1086
+ f"ground the response to; the input MMIF is malformed."
1087
+ )
1088
+ td.add_property('document', source_doc)
1059
1089
  if origins:
1060
1090
  td.add_property('origins', origins)
1061
1091
  td.add_property('origination', origination)
@@ -1065,13 +1095,52 @@ class ClamsPromptableApp(ClamsApp):
1065
1095
  target=td.id,
1066
1096
  )
1067
1097
  if reasoning_trace is not None:
1068
- raise NotImplementedError(
1069
- "Reasoning-trace storage convention is not yet defined; "
1070
- "tracked at clamsproject/clams-python#263."
1071
- )
1098
+ # Store the trace as a property on the TD rather than a separate
1099
+ # document: keeps the type-level output shape invariant (same
1100
+ # annotation, same `contains`, same graph) whether or not a trace is
1101
+ # present -- a strict enrichment, independent of whatever `response`
1102
+ # the caller put in `text`. Property name provisional pending
1103
+ # clamsproject/clams-vocabulary#11.
1104
+ td.add_property('modelReasoningTrace', reasoning_trace)
1072
1105
  return td, align
1073
1106
 
1074
-
1107
+ @staticmethod
1108
+ def split_tagged_reasoning_trace(
1109
+ text: str,
1110
+ open_tag: str = '<think>',
1111
+ close_tag: str = '</think>',
1112
+ ) -> Tuple[str, Optional[str]]:
1113
+ """
1114
+ Split a reasoning model's output into ``(answer, trace)`` for the
1115
+ inline XML-tag style: a single reasoning block delimited by
1116
+ ``open_tag`` / ``close_tag`` (default ``<think>`` / ``</think>``)
1117
+ embedded in the decoded text, e.g. as emitted by DeepSeek-R1 or
1118
+ Qwen3.
1119
+
1120
+ It does NOT handle models that isolate reasoning in a separate
1121
+ token-delimited channel rather than an inline tag block (e.g.
1122
+ gpt-oss, Gemma 4); those need their own parsing in the app. Trace
1123
+ handling is the app's responsibility either way -- call this when
1124
+ the model fits the inline-tag style, or parse it yourself.
1125
+
1126
+ ``answer`` is everything after the final ``close_tag`` (stripped);
1127
+ ``trace`` is the text between the tags, or ``None`` when no closed
1128
+ block is present -- so it is safe to call on non-reasoning output,
1129
+ which returns ``(text.strip(), None)``.
1130
+
1131
+ :param text: raw decoded model output.
1132
+ :param open_tag: opening marker of the inline reasoning block.
1133
+ :param close_tag: closing marker of the inline reasoning block.
1134
+ :return: ``(answer, trace_or_None)``.
1135
+ """
1136
+ ci = text.rfind(close_tag)
1137
+ if ci == -1:
1138
+ return text.strip(), None
1139
+ answer = text[ci + len(close_tag):].strip()
1140
+ oi = text.find(open_tag)
1141
+ trace = (text[oi + len(open_tag):ci].strip()
1142
+ if oi != -1 and oi < ci else None)
1143
+ return answer, trace
1075
1144
  class ClamsHFPromptableApp(ClamsPromptableApp):
1076
1145
  """
1077
1146
  Base class for promptable CLAMS apps backed by a local
@@ -1289,7 +1358,7 @@ class ClamsHFPromptableApp(ClamsPromptableApp):
1289
1358
  dtype=self.DTYPE,
1290
1359
  padding_side=self.PADDING_SIDE,
1291
1360
  revision=revision,
1292
- model_kwargs=self.MODEL_KWARGS,
1361
+ model_kwargs=self.model_load_kwargs(model_id, revision),
1293
1362
  processor_kwargs=self.PROCESSOR_KWARGS,
1294
1363
  )
1295
1364
  self.logger.info(f"HF model loaded on {triple[2]}")
@@ -1297,6 +1366,32 @@ class ClamsHFPromptableApp(ClamsPromptableApp):
1297
1366
  self.processor, self.model, self.device = triple
1298
1367
  return triple
1299
1368
 
1369
+ def model_load_kwargs(self, model_id: str, revision: str) -> dict:
1370
+ """
1371
+ The ``model_kwargs`` forwarded to
1372
+ :func:`~clams.backends.hf.load_hf_model` (and on to
1373
+ ``from_pretrained``) for a SPECIFIC family member. The base
1374
+ implementation returns a copy of the class-level
1375
+ :py:attr:`MODEL_KWARGS`, applied uniformly to every variant.
1376
+
1377
+ Override when a family mixes per-variant load requirements that
1378
+ a single class-level ``MODEL_KWARGS`` cannot express -- e.g. a
1379
+ ``analyzer_versions`` family whose members span full-precision,
1380
+ FP8, and GPTQ-Int4 builds, where only the quantized members need
1381
+ a ``quantization_config``::
1382
+
1383
+ def model_load_kwargs(self, model_id, revision):
1384
+ kw = super().model_load_kwargs(model_id, revision)
1385
+ if model_id.endswith('-GPTQ-Int4'):
1386
+ kw['quantization_config'] = ... # gptq config
1387
+ return kw
1388
+
1389
+ :param model_id: resolved HF model id (no ``@revision`` suffix).
1390
+ :param revision: resolved commit/revision being loaded.
1391
+ :return: ``model_kwargs`` dict for this variant.
1392
+ """
1393
+ return dict(self.MODEL_KWARGS or {})
1394
+
1300
1395
  def generate(
1301
1396
  self,
1302
1397
  prompt: List[str],
@@ -1341,6 +1436,7 @@ class ClamsHFPromptableApp(ClamsPromptableApp):
1341
1436
  if n == 0:
1342
1437
  return []
1343
1438
  gen_kwargs = self.build_gen_kwargs(**generation_params)
1439
+ template_kwargs = self.build_template_kwargs(**generation_params)
1344
1440
  try:
1345
1441
  conversations = [
1346
1442
  self.build_conversation(
@@ -1357,6 +1453,7 @@ class ClamsHFPromptableApp(ClamsPromptableApp):
1357
1453
  return_dict=True,
1358
1454
  padding=True,
1359
1455
  return_tensors="pt",
1456
+ **template_kwargs,
1360
1457
  )
1361
1458
  inputs = inputs.to(self.device)
1362
1459
  if (self.DTYPE is not None
@@ -1405,6 +1502,28 @@ class ClamsHFPromptableApp(ClamsPromptableApp):
1405
1502
  })
1406
1503
  return gen_kwargs
1407
1504
 
1505
+ def build_template_kwargs(self, **generation_params) -> dict:
1506
+ """
1507
+ Extra keyword arguments to pass to
1508
+ ``processor.apply_chat_template`` in :py:meth:`generate`, beyond
1509
+ the SDK-fixed ones (``add_generation_prompt``, ``tokenize``,
1510
+ ``return_dict``, ``padding``, ``return_tensors``). The base
1511
+ implementation returns ``{}``.
1512
+
1513
+ Override to inject model-specific chat-template controls without
1514
+ having to reimplement :py:meth:`generate`. Common cases:
1515
+
1516
+ * ``{'enable_thinking': False}`` to run a reasoning ("thinking")
1517
+ model in non-thinking mode;
1518
+ * ``{'tools': [...]}`` or ``{'documents': [...]}`` for tool-use /
1519
+ RAG chat templates.
1520
+
1521
+ Keys MUST NOT collide with the SDK-fixed kwargs above. Unknown
1522
+ keys are passed through to the template's render context, where
1523
+ a template that does not reference them simply ignores them.
1524
+ """
1525
+ return {}
1526
+
1408
1527
 
1409
1528
  class ParameterCaster(object):
1410
1529
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clams-python
3
- Version: 1.7.0
3
+ Version: 1.7.2
4
4
  Summary: A collection of APIs to develop CLAMS app for python
5
5
  License-Expression: Apache-2.0
6
6
  Project-URL: homepage, https://clams.ai
@@ -13,7 +13,7 @@ Classifier: Programming Language :: Python :: 3 :: Only
13
13
  Requires-Python: >=3.10
14
14
  Description-Content-Type: text/markdown
15
15
  License-File: LICENSE
16
- Requires-Dist: mmif-python==1.5.0
16
+ Requires-Dist: mmif-python==1.5.1
17
17
  Requires-Dist: Flask>=2
18
18
  Requires-Dist: Flask-RESTful>=0.3.9
19
19
  Requires-Dist: gunicorn>=20
@@ -12,9 +12,11 @@ pyproject.toml
12
12
  .github/workflows/issue-close.yml
13
13
  .github/workflows/issue-infra-project.yml
14
14
  .github/workflows/publish.yml
15
+ .github/workflows/release-check.yml
15
16
  build-tools/build.py
16
17
  build-tools/clean.py
17
18
  build-tools/docs.py
19
+ build-tools/prep_release.py
18
20
  build-tools/publish.py
19
21
  build-tools/test.py
20
22
  clams/__init__.py
@@ -48,6 +50,8 @@ clams_python.egg-info/SOURCES.txt
48
50
  clams_python.egg-info/dependency_links.txt
49
51
  clams_python.egg-info/entry_points.txt
50
52
  clams_python.egg-info/requires.txt
53
+ clams_python.egg-info/scm_file_list.json
54
+ clams_python.egg-info/scm_version.json
51
55
  clams_python.egg-info/top_level.txt
52
56
  container/Containerfile
53
57
  container/ffmpeg-hf.containerfile
@@ -85,6 +89,7 @@ documentation/target-versions.csv
85
89
  documentation/tutorial.md
86
90
  documentation/autodoc/clams.app.rst
87
91
  documentation/autodoc/clams.appmetadata.rst
92
+ documentation/autodoc/clams.backends.rst
88
93
  documentation/autodoc/clams.mmif_utils.rst
89
94
  documentation/autodoc/clams.restify.rst
90
95
  tests/metadata.py
@@ -1,4 +1,4 @@
1
- mmif-python==1.5.0
1
+ mmif-python==1.5.1
2
2
  Flask>=2
3
3
  Flask-RESTful>=0.3.9
4
4
  gunicorn>=20
@@ -0,0 +1,96 @@
1
+ {
2
+ "files": [
3
+ "README.md",
4
+ "LICENSE",
5
+ "pyproject.toml",
6
+ ".pytype.cfg",
7
+ "CHANGELOG.md",
8
+ "CONTRIBUTING.md",
9
+ ".gitignore",
10
+ "build-tools/publish.py",
11
+ "build-tools/prep_release.py",
12
+ "build-tools/build.py",
13
+ "build-tools/docs.py",
14
+ "build-tools/clean.py",
15
+ "build-tools/test.py",
16
+ "clams/__init__.py",
17
+ "clams/develop/__init__.py",
18
+ "clams/develop/templates/gha/for-clams-team.md.template",
19
+ "clams/develop/templates/gha/workflows/issue-assign.yml.template",
20
+ "clams/develop/templates/gha/workflows/publish.yml.template",
21
+ "clams/develop/templates/gha/workflows/issue-apps-project.yml.template",
22
+ "clams/develop/templates/gha/workflows/issue-close.yml.template",
23
+ "clams/develop/templates/utl-tf/__init__.py.template",
24
+ "clams/develop/templates/utl-tf/timeframe.py.template",
25
+ "clams/develop/templates/app/Containerfile.template",
26
+ "clams/develop/templates/app/metadata.py.template",
27
+ "clams/develop/templates/app/README.md.template",
28
+ "clams/develop/templates/app/.dockerignore.template",
29
+ "clams/develop/templates/app/requirements.txt.template",
30
+ "clams/develop/templates/app/LICENSE.template",
31
+ "clams/develop/templates/app/.gitignore.template",
32
+ "clams/develop/templates/app/app.py.template",
33
+ "clams/develop/templates/app/cli.py.template",
34
+ "clams/restify/__init__.py",
35
+ "clams/envelop/__init__.py",
36
+ "clams/appmetadata/__init__.py",
37
+ "clams/app/__init__.py",
38
+ "clams/serve/__init__.py",
39
+ "clams/ver/__init__.py",
40
+ "clams/backends/__init__.py",
41
+ "clams/backends/hf.py",
42
+ "documentation/modules.rst",
43
+ "documentation/target-versions.csv",
44
+ "documentation/app-baseclasses.rst",
45
+ "documentation/gpu-apps.rst",
46
+ "documentation/clamsapp.rst",
47
+ "documentation/appdirectory.rst",
48
+ "documentation/index.rst",
49
+ "documentation/tutorial.md",
50
+ "documentation/introduction.rst",
51
+ "documentation/appmetadata.rst",
52
+ "documentation/cli.rst",
53
+ "documentation/runtime-params.rst",
54
+ "documentation/input-output.rst",
55
+ "documentation/conf.py",
56
+ "documentation/autodoc/clams.restify.rst",
57
+ "documentation/autodoc/clams.app.rst",
58
+ "documentation/autodoc/clams.backends.rst",
59
+ "documentation/autodoc/clams.mmif_utils.rst",
60
+ "documentation/autodoc/clams.appmetadata.rst",
61
+ "tests/test_promptable.py",
62
+ "tests/pillow-logo.png",
63
+ "tests/test_backends_hf.py",
64
+ "tests/test_envelope.py",
65
+ "tests/metadata.py",
66
+ "tests/test_clamsapp.py",
67
+ "container/tf2.containerfile",
68
+ "container/opencv4-hf.containerfile",
69
+ "container/torch2.containerfile",
70
+ "container/opencv4-hf4.containerfile",
71
+ "container/Containerfile",
72
+ "container/hf4.containerfile",
73
+ "container/ffmpeg-hf.containerfile",
74
+ "container/ffmpeg-tf2.containerfile",
75
+ "container/generate_containers_yml.py",
76
+ "container/opencv4-tf2.containerfile",
77
+ "container/opencv4-torch2.containerfile",
78
+ "container/hf.containerfile",
79
+ "container/jdk8.containerfile",
80
+ "container/hf5.containerfile",
81
+ "container/ffmpeg.containerfile",
82
+ "container/ffmpeg-hf5.containerfile",
83
+ "container/ffmpeg-hf4.containerfile",
84
+ "container/opencv4-hf5.containerfile",
85
+ "container/ffmpeg-torch2.containerfile",
86
+ "container/opencv4.containerfile",
87
+ ".github/workflows/container.yml",
88
+ ".github/workflows/containers.yml",
89
+ ".github/workflows/issue-close.yml",
90
+ ".github/workflows/codecov.yml",
91
+ ".github/workflows/release-check.yml",
92
+ ".github/workflows/issue-infra-project.yml",
93
+ ".github/workflows/issue-assign.yml",
94
+ ".github/workflows/publish.yml"
95
+ ]
96
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "1.7.2",
3
+ "distance": 0,
4
+ "node": "g874f55164c482ad4c7896c6bfd8d6a69649eb66b",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-07-07"
8
+ }
@@ -0,0 +1,15 @@
1
+ clams.backends package
2
+ ======================
3
+
4
+ .. automodule:: clams.backends
5
+
6
+ Submodules
7
+ ----------
8
+
9
+ clams.backends.hf
10
+ ^^^^^^^^^^^^^^^^^
11
+
12
+ .. automodule:: clams.backends.hf
13
+ :members:
14
+ :undoc-members:
15
+ :show-inheritance:
@@ -14,8 +14,6 @@ import datetime
14
14
  import inspect
15
15
  import json
16
16
  import os
17
- import re
18
- import shutil
19
17
  import subprocess
20
18
  import sys
21
19
  from pathlib import Path
@@ -196,24 +194,6 @@ def generate_jsonschema(app):
196
194
  json.dump(schema_dict, f, indent=2)
197
195
 
198
196
 
199
- def update_target_spec(app):
200
- target_vers_csv = Path(__file__).parent / 'target-versions.csv'
201
- version = _get_version('clams-python')
202
- # Skip dev/dummy versions to avoid dirtying the git-tracked CSV
203
- if 'dev' in version or not re.match(r'^\d+\.\d+\.\d+$', version):
204
- return
205
- mmifver = mmif.__version__
206
- specver = mmif.__specver__
207
- with open(target_vers_csv) as in_f, open(f'{target_vers_csv}.new', 'w') as out_f:
208
- lines = in_f.readlines()
209
- if not lines[1].startswith(f"`{version}"):
210
- lines.insert(1, f"`{version} <https://pypi.org/project/clams-python/{version}/>`__,`{mmifver} <https://pypi.org/project/mmif-python/{mmifver}/>`__,`{specver} <https://mmif.clams.ai/{specver}/>`__\n")
211
- for line in lines:
212
- out_f.write(line)
213
- shutil.move(out_f.name, in_f.name)
214
-
215
-
216
197
  def setup(app):
217
198
  app.connect('builder-inited', generate_whatsnew_rst)
218
199
  app.connect('builder-inited', generate_jsonschema)
219
- app.connect('builder-inited', update_target_spec)
@@ -1,4 +1,10 @@
1
1
  "``clams-python`` version","``mmif-python`` version","Target MMIF Specification"
2
+ `1.7.2 <https://pypi.org/project/clams-python/1.7.2/>`__,`1.5.1 <https://pypi.org/project/mmif-python/1.5.1/>`__,`1.2.0 <https://mmif.clams.ai/1.2.0/>`__
3
+ `1.7.1 <https://pypi.org/project/clams-python/1.7.1/>`__,`1.5.1 <https://pypi.org/project/mmif-python/1.5.1/>`__,`1.2.0 <https://mmif.clams.ai/1.2.0/>`__
4
+ `1.7.0 <https://pypi.org/project/clams-python/1.7.0/>`__,`1.5.0 <https://pypi.org/project/mmif-python/1.5.0/>`__,`1.2.0 <https://mmif.clams.ai/1.2.0/>`__
5
+ `1.6.0 <https://pypi.org/project/clams-python/1.6.0/>`__,`1.4.0 <https://pypi.org/project/mmif-python/1.4.0/>`__,`1.1.1 <https://mmif.clams.ai/1.1.1/>`__
6
+ `1.5.0 <https://pypi.org/project/clams-python/1.5.0/>`__,`1.3.1 <https://pypi.org/project/mmif-python/1.3.1/>`__,`1.1.0 <https://mmif.clams.ai/1.1.0/>`__
7
+ `1.4.0 <https://pypi.org/project/clams-python/1.4.0/>`__,`1.2.1 <https://pypi.org/project/mmif-python/1.2.1/>`__,`1.1.0 <https://mmif.clams.ai/1.1.0/>`__
2
8
  `1.3.3 <https://pypi.org/project/clams-python/1.3.3/>`__,`1.1.2 <https://pypi.org/project/mmif-python/1.1.2/>`__,`1.1.0 <https://mmif.clams.ai/1.1.0/>`__
3
9
  `1.3.2 <https://pypi.org/project/clams-python/1.3.2/>`__,`1.1.1 <https://pypi.org/project/mmif-python/1.1.1/>`__,`1.1.0 <https://mmif.clams.ai/1.1.0/>`__
4
10
  `1.3.1 <https://pypi.org/project/clams-python/1.3.1/>`__,`1.0.19 <https://pypi.org/project/mmif-python/1.0.19/>`__,`1.0.5 <https://mmif.clams.ai/1.0.5/>`__
@@ -50,4 +56,4 @@
50
56
  `0.1.7 <https://pypi.org/project/clams-python/0.1.7/>`__,`0.2.2 <https://pypi.org/project/mmif-python/0.2.2/>`__,`0.2.1 <https://mmif.clams.ai/0.2.1/>`__
51
57
  `0.1.6 <https://pypi.org/project/clams-python/0.1.6/>`__,`0.2.1 <https://pypi.org/project/mmif-python/0.2.1/>`__,`0.2.1 <https://mmif.clams.ai/0.2.1/>`__
52
58
  `0.1.5 <https://pypi.org/project/clams-python/0.1.5/>`__,`0.2.1 <https://pypi.org/project/mmif-python/0.2.1/>`__,`0.2.1 <https://mmif.clams.ai/0.2.1/>`__
53
- `0.1.4 <https://pypi.org/project/clams-python/0.1.4/>`__,`0.2.1 <https://pypi.org/project/mmif-python/0.2.1/>`__,`0.2.1 <https://mmif.clams.ai/0.2.1/>`__
59
+ `0.1.4 <https://pypi.org/project/clams-python/0.1.4/>`__,`0.2.1 <https://pypi.org/project/mmif-python/0.2.1/>`__,`0.2.1 <https://mmif.clams.ai/0.2.1/>`__
@@ -19,7 +19,7 @@ classifiers = [
19
19
  "Programming Language :: Python :: 3 :: Only",
20
20
  ]
21
21
  dependencies = [
22
- "mmif-python==1.5.0",
22
+ "mmif-python==1.5.1",
23
23
  "Flask>=2",
24
24
  "Flask-RESTful>=0.3.9",
25
25
  "gunicorn>=20",
@@ -10,7 +10,7 @@ single-turn / turn-taking / user-only modes, and the
10
10
  """
11
11
  import unittest
12
12
 
13
- from mmif import AnnotationTypes, DocumentTypes, Mmif
13
+ from mmif import AnnotationTypes, Document, DocumentTypes, Mmif
14
14
 
15
15
  from clams import AppMetadata, ClamsPromptableApp
16
16
 
@@ -326,6 +326,19 @@ class TestStoreResponse(unittest.TestCase):
326
326
  def setUp(self):
327
327
  self.app = make_test_app(make_metadata(call_helper=True))
328
328
  self.mmif = Mmif(validate=False)
329
+ # upstream: a media document and an annotation anchored to it, which
330
+ # serves as the response's `source`.
331
+ vdoc = Document()
332
+ vdoc.at_type = DocumentTypes.VideoDocument
333
+ vdoc.id = 'v1'
334
+ vdoc.location = 'file:///video.mp4'
335
+ self.mmif.add_document(vdoc)
336
+ self.doc_id = vdoc.id
337
+ src_view = self.mmif.new_view()
338
+ src_view.metadata.app = 'http://upstream/1'
339
+ self.src = src_view.new_annotation(
340
+ AnnotationTypes.TimeFrame, document=vdoc.id, label='scene')
341
+ # the current app's view
329
342
  self.view = self.mmif.new_view()
330
343
  self.app.sign_view(self.view, {})
331
344
  self.view.new_contain(DocumentTypes.TextDocument)
@@ -333,22 +346,34 @@ class TestStoreResponse(unittest.TestCase):
333
346
 
334
347
  def test_happy_path_creates_textdocument_and_alignment(self):
335
348
  td, align = self.app.response_to_grounded_textdocument(
336
- self.view, source='src1', response='generated text')
349
+ self.view, source=self.src.id, response='generated text')
337
350
  self.assertEqual(td.text_value, 'generated text')
338
- self.assertEqual(align.get_property('source'), 'src1')
351
+ # the TD inherits the source annotation's document
352
+ self.assertEqual(td.get_property('document'), self.doc_id)
353
+ self.assertEqual(align.get_property('source'), self.src.id)
339
354
  self.assertEqual(align.get_property('target'), td.id)
340
355
 
341
- def test_reasoning_trace_none_does_not_raise(self):
342
- # no exception
343
- self.app.response_to_grounded_textdocument(
344
- self.view, source='src1', response='text',
356
+ def test_source_without_document_raises(self):
357
+ # a source annotation carrying no `document` means a malformed input
358
+ ungrounded = self.view.new_annotation(AnnotationTypes.TimeFrame)
359
+ with self.assertRaises(ValueError):
360
+ self.app.response_to_grounded_textdocument(
361
+ self.view, source=ungrounded.id, response='text')
362
+
363
+ def test_reasoning_trace_none_stores_no_property(self):
364
+ td, _ = self.app.response_to_grounded_textdocument(
365
+ self.view, source=self.src.id, response='text',
345
366
  reasoning_trace=None)
367
+ self.assertNotIn('modelReasoningTrace', td.properties)
346
368
 
347
- def test_reasoning_trace_not_none_raises_not_implemented(self):
348
- with self.assertRaises(NotImplementedError):
349
- self.app.response_to_grounded_textdocument(
350
- self.view, source='src1', response='text',
351
- reasoning_trace='intermediate reasoning')
369
+ def test_reasoning_trace_stored_on_textdocument(self):
370
+ td, _ = self.app.response_to_grounded_textdocument(
371
+ self.view, source=self.src.id, response='the answer',
372
+ reasoning_trace='step 1 ... step 2 ...')
373
+ # trace lives in the property; the TD text stays answer-only
374
+ self.assertEqual(
375
+ td.get_property('modelReasoningTrace'), 'step 1 ... step 2 ...')
376
+ self.assertEqual(td.text_value, 'the answer')
352
377
 
353
378
  # TODO (krim @ 05/28/26): this test case belongs upstream in the
354
379
  # vocabulary type definition (the `origins`/`origination` pairing
@@ -359,11 +384,11 @@ class TestStoreResponse(unittest.TestCase):
359
384
  # underlying TD.
360
385
  def test_origins_and_origination_written_together(self):
361
386
  td, align = self.app.response_to_grounded_textdocument(
362
- self.view, source='tf1', response='caption text',
387
+ self.view, source=self.src.id, response='caption text',
363
388
  origins=['tp1'], origination='derived')
364
389
  self.assertEqual(td.get_property('origins'), ['tp1'])
365
390
  self.assertEqual(td.get_property('origination'), 'derived')
366
- self.assertEqual(align.get_property('source'), 'tf1')
391
+ self.assertEqual(align.get_property('source'), self.src.id)
367
392
  self.assertEqual(align.get_property('target'), td.id)
368
393
 
369
394
  def test_unpaired_origins_or_origination_raises(self):
@@ -377,6 +402,51 @@ class TestStoreResponse(unittest.TestCase):
377
402
  self.view, source='src1', response='text', **kwargs)
378
403
 
379
404
 
405
+ # ---------------------------------------------------------------------------
406
+ # Reasoning-trace split helper
407
+ # ---------------------------------------------------------------------------
408
+
409
+ class TestSplitReasoningTrace(unittest.TestCase):
410
+ """:meth:`ClamsPromptableApp.split_tagged_reasoning_trace` (static)."""
411
+
412
+ split = staticmethod(ClamsPromptableApp.split_tagged_reasoning_trace)
413
+
414
+ def test_closed_block_splits_answer_and_trace(self):
415
+ answer, trace = self.split('<think>reasoning</think>The answer.')
416
+ self.assertEqual(answer, 'The answer.')
417
+ self.assertEqual(trace, 'reasoning')
418
+
419
+ def test_no_tags_returns_text_as_answer_none_trace(self):
420
+ answer, trace = self.split('just an answer')
421
+ self.assertEqual(answer, 'just an answer')
422
+ self.assertIsNone(trace)
423
+
424
+ def test_unterminated_block_not_overstripped(self):
425
+ # No closing tag (e.g. ran out of tokens mid-thought): return the
426
+ # raw text rather than discard a possibly-real answer.
427
+ raw = '<think>thinking with no close'
428
+ answer, trace = self.split(raw)
429
+ self.assertEqual(answer, raw)
430
+ self.assertIsNone(trace)
431
+
432
+ def test_uses_final_close_tag(self):
433
+ answer, trace = self.split(
434
+ '<think>a</think>mid<think>b</think>final')
435
+ self.assertEqual(answer, 'final')
436
+
437
+ def test_custom_tags(self):
438
+ answer, trace = self.split(
439
+ '[R]why[/R]done', open_tag='[R]', close_tag='[/R]')
440
+ self.assertEqual(answer, 'done')
441
+ self.assertEqual(trace, 'why')
442
+
443
+ def test_non_thinking_output_safe_to_call(self):
444
+ # Safe to call unconditionally even when thinking is disabled.
445
+ answer, trace = self.split(' plain caption ')
446
+ self.assertEqual(answer, 'plain caption')
447
+ self.assertIsNone(trace)
448
+
449
+
380
450
  # ---------------------------------------------------------------------------
381
451
  # Transport-neutral parameter casting
382
452
  # ---------------------------------------------------------------------------
@@ -580,6 +650,56 @@ class TestHFPromptableAppClassAttrs(unittest.TestCase):
580
650
  finally:
581
651
  restore()
582
652
 
653
+ def test_build_template_kwargs_default_empty(self):
654
+ restore, _ = self._patch_load()
655
+ try:
656
+ app = self._make_subclass(analyzer_versions=self.SINGLETON_AV)()
657
+ self.assertEqual(app.build_template_kwargs(), {})
658
+ finally:
659
+ restore()
660
+
661
+ def test_model_load_kwargs_default_is_model_kwargs_copy(self):
662
+ restore, calls = self._patch_load()
663
+ try:
664
+ app = self._make_subclass(
665
+ analyzer_versions=self.SINGLETON_AV,
666
+ MODEL_KWARGS={'trust_remote_code': True},
667
+ )()
668
+ # eager singleton load forwarded the class-level MODEL_KWARGS
669
+ self.assertEqual(
670
+ calls[0]['model_kwargs'], {'trust_remote_code': True})
671
+ # and it's a copy, not the same object
672
+ self.assertIsNot(
673
+ app.model_load_kwargs('org/fake-model', 'deadbee'),
674
+ app.MODEL_KWARGS)
675
+ finally:
676
+ restore()
677
+
678
+ def test_model_load_kwargs_override_is_per_variant(self):
679
+ restore, calls = self._patch_load()
680
+ try:
681
+ def per_variant(self, model_id, revision):
682
+ kw = {'base': True}
683
+ if model_id.endswith('small-model'):
684
+ kw['quantized'] = True
685
+ return kw
686
+
687
+ cls = self._make_subclass(
688
+ analyzer_versions=self.MULTI_AV,
689
+ model_load_kwargs=per_variant,
690
+ )
691
+ app = cls() # multi-member: no eager load
692
+ self.assertEqual(len(calls), 0)
693
+ app.load_model('org/large-model')
694
+ app.load_model('org/small-model')
695
+ large = next(c for c in calls if c['model_id'] == 'org/large-model')
696
+ small = next(c for c in calls if c['model_id'] == 'org/small-model')
697
+ self.assertEqual(large['model_kwargs'], {'base': True})
698
+ self.assertEqual(
699
+ small['model_kwargs'], {'base': True, 'quantized': True})
700
+ finally:
701
+ restore()
702
+
583
703
 
584
704
  if __name__ == '__main__':
585
705
  unittest.main()
File without changes
File without changes
File without changes
File without changes
File without changes