unique_toolkit 2026.34.0.dev0__py3-none-any.whl → 2026.34.0.dev2__py3-none-any.whl

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.
@@ -1,7 +1,8 @@
1
1
  import asyncio
2
2
  import logging
3
3
  import time
4
- from typing import Any, cast
4
+ from datetime import datetime
5
+ from typing import Any, NamedTuple, cast
5
6
 
6
7
  from typing_extensions import deprecated
7
8
  from unique_sdk import (
@@ -49,6 +50,56 @@ def _sheet_batch_cells_include_row_metadata_from_api(
49
50
  return "rowMetadata" in first and isinstance(rm, list)
50
51
 
51
52
 
53
+ def _artifact_timestamp(value: str) -> datetime:
54
+ """Parse an artifact ISO-8601 timestamp (e.g. ``2026-07-31T16:26:36.344Z``)."""
55
+ return datetime.fromisoformat(value.replace("Z", "+00:00"))
56
+
57
+
58
+ class _ArtifactBaseline(NamedTuple):
59
+ """What a sheet's artifacts of one type looked like just before an export of
60
+ that type was triggered.
61
+
62
+ ``ids`` are the records of the type that already existed; ``latest`` is
63
+ their newest ``updated_at``. Two signals because either can carry the
64
+ result: a finished report normally arrives as a new record (the trigger's
65
+ nameless IN_PROGRESS marker and the named report are separate rows), but a
66
+ re-export can also refresh an existing record in place.
67
+
68
+ Kept per artifact type so that triggering one type does not fence out an
69
+ already-completed export of another type.
70
+
71
+ Mirrors the ``_ArtifactBaseline`` used by the SDK CLI's export wait.
72
+ """
73
+
74
+ ids: frozenset[str]
75
+ latest: datetime | None
76
+
77
+
78
+ def _snapshot_artifacts(
79
+ artifacts: list[MagicTableArtifact],
80
+ artifact_types: list[MagicTableArtifactType],
81
+ ) -> dict[MagicTableArtifactType, _ArtifactBaseline]:
82
+ baselines: dict[MagicTableArtifactType, _ArtifactBaseline] = {}
83
+ for artifact_type in artifact_types:
84
+ records = [a for a in artifacts if a.artifact_type == artifact_type]
85
+ timestamps = [_artifact_timestamp(a.updated_at) for a in records]
86
+ baselines[artifact_type] = _ArtifactBaseline(
87
+ ids=frozenset(a.id for a in records),
88
+ latest=max(timestamps) if timestamps else None,
89
+ )
90
+ return baselines
91
+
92
+
93
+ def _is_fresh(artifact: MagicTableArtifact, baseline: _ArtifactBaseline) -> bool:
94
+ """Whether *artifact* came out of a generation triggered after *baseline*."""
95
+ if artifact.id not in baseline.ids:
96
+ return True
97
+ return (
98
+ baseline.latest is not None
99
+ and _artifact_timestamp(artifact.updated_at) > baseline.latest
100
+ )
101
+
102
+
52
103
  class LockedAgenticTableError(Exception):
53
104
  pass
54
105
 
@@ -88,6 +139,9 @@ class AgenticTableService:
88
139
  self.table_id = table_id
89
140
  self.logger = logger
90
141
  self.created_sheet: CreatedMagicTableSheet | None = None
142
+ # Per-type snapshots taken by generate_artifacts so wait_for_artifacts can
143
+ # tell a new export apart from records left by earlier generations.
144
+ self._artifact_baselines: dict[MagicTableArtifactType, _ArtifactBaseline] = {}
91
145
 
92
146
  @classmethod
93
147
  async def create_sheet(
@@ -634,9 +688,17 @@ class AgenticTableService:
634
688
  (or poll ``list_artifacts``) to detect completion, then download the artifact's
635
689
  ``content_id`` via the Content API.
636
690
 
691
+ Snapshots the sheet's artifacts of the requested types before
692
+ triggering, so a following ``wait_for_artifacts`` on this instance can
693
+ tell the new export apart from records left over from earlier
694
+ generations. Only the requested types' snapshots are replaced; a
695
+ baseline armed by an earlier trigger for another type is kept, so its
696
+ completed export stays recognisable.
697
+
637
698
  Raises:
638
699
  Exception: If the API reports a non-success status.
639
700
  """
701
+ baselines = _snapshot_artifacts(await self.list_artifacts(), artifact_types)
640
702
  result = await AgenticTable.generate_artifact(
641
703
  user_id=self._user_id,
642
704
  company_id=self._company_id,
@@ -647,6 +709,7 @@ class AgenticTableService:
647
709
  raise Exception(
648
710
  result.get("message") or "Failed to trigger artifact generation"
649
711
  )
712
+ self._artifact_baselines.update(baselines)
650
713
 
651
714
  async def rerun_row(self, row_order: int) -> None:
652
715
  """Re-run the agent for a single row (`POST .../row/{rowOrder}/rerun`).
@@ -798,15 +861,26 @@ class AgenticTableService:
798
861
  ) -> list[MagicTableArtifact]:
799
862
  """Wait until artifacts of the given types are DONE, by polling ``list_artifacts``.
800
863
 
801
- Artifact records are upserted per type: triggering an export flips the record
802
- of that type to IN_PROGRESS and back to DONE when the file is ready. A
803
- terminal state is accepted only after IN_PROGRESS has been observed for
804
- that artifact type, so a result left over from an earlier generation is
805
- not mistaken for the newly triggered one.
806
-
807
- Because the API exposes only the current artifact state, a complete
808
- IN_PROGRESS-to-terminal transition between two polls cannot be observed.
809
- Keep ``poll_interval`` shorter than the expected minimum generation time.
864
+ A sheet can hold several records of the same type: the export trigger
865
+ records a nameless IN_PROGRESS marker that is not always the record the
866
+ worker completes, so a stale IN_PROGRESS row can sit next to the DONE
867
+ result, and reports from earlier generations remain listed. Two fences
868
+ keep the wait from mistaking leftovers for the new export:
869
+
870
+ - For a type that ``generate_artifacts`` was called for on this
871
+ instance, only records that are *fresh* relative to that trigger's
872
+ snapshot count: a record id that did not exist before, or an
873
+ ``updated_at`` newer than the snapshot. This also accepts exports
874
+ that finish before the first poll.
875
+ - For a type without a snapshot (export triggered elsewhere), a
876
+ terminal state is accepted only after IN_PROGRESS has been observed
877
+ for that type, and the newest record (by ``updated_at``) decides the
878
+ state. This cannot distinguish a result of an earlier generation
879
+ whose trigger marker is still IN_PROGRESS — prefer triggering and
880
+ waiting on the same service instance.
881
+
882
+ Because the API exposes only the current artifact state, keep
883
+ ``poll_interval`` shorter than the expected minimum generation time.
810
884
 
811
885
  Args:
812
886
  artifact_types: The artifact types to wait for (as passed to ``generate_artifacts``).
@@ -821,22 +895,41 @@ class AgenticTableService:
821
895
  AgenticTableRunTimeoutError: Not all artifacts were DONE within ``timeout``.
822
896
  """
823
897
  wanted = set(artifact_types)
898
+ baselines = {
899
+ artifact_type: baseline
900
+ for artifact_type, baseline in self._artifact_baselines.items()
901
+ if artifact_type in wanted
902
+ }
824
903
  started: set[MagicTableArtifactType] = set()
825
904
  deadline = time.monotonic() + timeout
826
905
  while True:
827
906
  artifacts = await self.list_artifacts()
828
- current = {
829
- a.artifact_type: a for a in artifacts if a.artifact_type in wanted
907
+ relevant = [a for a in artifacts if a.artifact_type in wanted]
908
+ candidates: list[MagicTableArtifact] = []
909
+ for artifact in relevant:
910
+ baseline = baselines.get(artifact.artifact_type)
911
+ if baseline is None:
912
+ candidates.append(artifact)
913
+ if artifact.artifact_state == MagicTableArtifactState.IN_PROGRESS:
914
+ started.add(artifact.artifact_type)
915
+ elif _is_fresh(artifact, baseline):
916
+ candidates.append(artifact)
917
+ gate = started | {
918
+ a.artifact_type for a in candidates if a.artifact_type in baselines
830
919
  }
831
- started.update(
832
- artifact_type
833
- for artifact_type, artifact in current.items()
834
- if artifact.artifact_state == MagicTableArtifactState.IN_PROGRESS
835
- )
920
+ # Newest record per type decides the state; older duplicates (e.g. the
921
+ # stale nameless IN_PROGRESS row from the export trigger) are ignored.
922
+ current: dict[MagicTableArtifactType, MagicTableArtifact] = {}
923
+ for artifact in candidates:
924
+ newest = current.get(artifact.artifact_type)
925
+ if newest is None or _artifact_timestamp(
926
+ artifact.updated_at
927
+ ) > _artifact_timestamp(newest.updated_at):
928
+ current[artifact.artifact_type] = artifact
836
929
  failed = [
837
930
  artifact
838
931
  for artifact_type, artifact in current.items()
839
- if artifact_type in started
932
+ if artifact_type in gate
840
933
  and artifact.artifact_state == MagicTableArtifactState.ERROR
841
934
  ]
842
935
  if failed:
@@ -847,7 +940,7 @@ class AgenticTableService:
847
940
  done = {
848
941
  artifact_type: artifact
849
942
  for artifact_type, artifact in current.items()
850
- if artifact_type in started
943
+ if artifact_type in gate
851
944
  and artifact.artifact_state == MagicTableArtifactState.DONE
852
945
  }
853
946
  if wanted <= set(done.keys()):
@@ -104,10 +104,12 @@ class LanguageModelName(StrEnum):
104
104
  GEMINI_2_5_PRO_EXP_0325 = "litellm:gemini-2-5-pro-exp-03-25"
105
105
  GEMINI_2_5_PRO_PREVIEW_0605 = "litellm:gemini-2-5-pro-preview-06-05"
106
106
  GEMINI_3_1_PRO_PREVIEW = "litellm:gemini-3-1-pro-preview"
107
+ GEMINI_3_6_FLASH = "litellm:gemini-3-6-flash"
107
108
  GEMINI_3_FLASH_PREVIEW = "litellm:gemini-3-flash-preview"
108
109
  GEMINI_3_PRO_PREVIEW = "litellm:gemini-3-pro-preview"
109
110
  GROK_4_1_FAST_NON_REASONING = "litellm:grok-4-1-fast-non-reasoning"
110
111
  GROK_4_1_FAST_REASONING = "litellm:grok-4-1-fast-reasoning"
112
+ GROK_4_5 = "litellm:grok-4-5"
111
113
  LITELLM_OPENAI_GPT_5 = "litellm:openai-gpt-5"
112
114
  LITELLM_OPENAI_GPT_5_MINI = "litellm:openai-gpt-5-mini"
113
115
  LITELLM_OPENAI_GPT_5_NANO = "litellm:openai-gpt-5-nano"
@@ -2052,6 +2054,28 @@ class LanguageModelInfo(BaseModel):
2052
2054
  published_at=date(2026, 1, 19),
2053
2055
  supported_reasoning_efforts=[],
2054
2056
  )
2057
+ case LanguageModelName.GEMINI_3_6_FLASH:
2058
+ return cls(
2059
+ name=model_name,
2060
+ capabilities=[
2061
+ ModelCapabilities.FUNCTION_CALLING,
2062
+ ModelCapabilities.STREAMING,
2063
+ ModelCapabilities.VISION,
2064
+ ModelCapabilities.STRUCTURED_OUTPUT,
2065
+ ModelCapabilities.REASONING,
2066
+ ModelCapabilities.VISION,
2067
+ ],
2068
+ provider=LanguageModelProvider.LITELLM,
2069
+ family=ModelFamily.GOOGLE,
2070
+ version="gemini-3-6-flash",
2071
+ encoder_name=EncoderName.O200K_BASE, # TODO: Update encoder with litellm
2072
+ token_limits=LanguageModelTokenLimits(
2073
+ token_limit_input=1_048_576, token_limit_output=65_536
2074
+ ),
2075
+ info_cutoff_at=date(2026, 3, day=1),
2076
+ published_at=date(2026, 7, 21),
2077
+ supported_reasoning_efforts=["minimal", "low", "medium", "high"],
2078
+ )
2055
2079
  case LanguageModelName.GEMINI_3_FLASH_PREVIEW:
2056
2080
  return cls(
2057
2081
  name=model_name,
@@ -2139,6 +2163,30 @@ class LanguageModelInfo(BaseModel):
2139
2163
  published_at=date(2025, 11, 19),
2140
2164
  supported_reasoning_efforts=[],
2141
2165
  )
2166
+ case LanguageModelName.GROK_4_5:
2167
+ return cls(
2168
+ name=model_name,
2169
+ capabilities=[
2170
+ ModelCapabilities.FUNCTION_CALLING,
2171
+ ModelCapabilities.REASONING,
2172
+ ModelCapabilities.STREAMING,
2173
+ ModelCapabilities.STRUCTURED_OUTPUT,
2174
+ ModelCapabilities.VISION,
2175
+ ],
2176
+ provider=LanguageModelProvider.LITELLM,
2177
+ family=ModelFamily.XAI,
2178
+ version="grok-4-5",
2179
+ encoder_name=EncoderName.O200K_BASE, # TODO: Update encoder with grok tokenizer
2180
+ token_limits=LanguageModelTokenLimits(
2181
+ # Context window is 500_000, we leave 20_000 tokens as buffer due to tokenizer mismatch
2182
+ # Assign 90% for input and 10% for output
2183
+ token_limit_input=int(480_000 * 0.9),
2184
+ token_limit_output=int(480_000 * 0.1),
2185
+ ),
2186
+ info_cutoff_at=date(2026, 2, day=1),
2187
+ published_at=date(2026, 7, 16),
2188
+ supported_reasoning_efforts=["low", "medium", "high"],
2189
+ )
2142
2190
  case LanguageModelName.LITELLM_OPENAI_GPT_5:
2143
2191
  return cls(
2144
2192
  name=model_name,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: unique-toolkit
3
- Version: 2026.34.0.dev0
3
+ Version: 2026.34.0.dev2
4
4
  Summary:
5
5
  Author: Cedric Klinkert, Martin Fadler, Sadique Sheik, Fabian Schläpfer, Pascal Hauri, Ejiroghene Oki-Peter, Rami Azouz, Ricardo Visini, Sanzio Monti
6
6
  Author-email: Cedric Klinkert <cedric.klinkert@unique.ch>, Martin Fadler <martin.fadler@unique.ch>, Sadique Sheik <sadique@unique.ch>, Fabian Schläpfer <fabian@unique.ch>, Pascal Hauri <pascal@unique.ch>, Ejiroghene Oki-Peter <oki-peter.ejiroghene@unique.ai>, Rami Azouz <rami.ext@unique.ch>, Ricardo Visini <ricardo.visini@unique.ch>, Sanzio Monti <sanzio.monti@unique.ai>
@@ -238,7 +238,7 @@ unique_toolkit/agentic/tools/utils/source_handling/source_formatting.py,sha256=u
238
238
  unique_toolkit/agentic/tools/utils/source_handling/tests/test_source_formatting.py,sha256=EA8iVvb3L91OFk2XMbGcFuhe2etqm3Sx9QCYDGiOSOM,6995
239
239
  unique_toolkit/agentic_table/__init__.py,sha256=4WenUtjX3IPA88ljNAkfcckVMy9P8jUHR7LnaoLx6ok,2240
240
240
  unique_toolkit/agentic_table/schemas.py,sha256=GobP1gcWSiwX96wTG7c-Su2NoWBy191HWGXY5bB2_qU,14161
241
- unique_toolkit/agentic_table/service.py,sha256=cS-tPfePeZpDjvI9hgCIZnA11fxprIfeFNFfZI6VFJo,33722
241
+ unique_toolkit/agentic_table/service.py,sha256=WOX47T5dHj7yvNWCXEKXccVx5Kb-q-jADHSbU5luwxE,38298
242
242
  unique_toolkit/agentic_table/synthetic_chat_event.py,sha256=MylvYuhx5CVt2DbPetzV0sR8GlkL218DmNFi7A6RYVs,2373
243
243
  unique_toolkit/app/__init__.py,sha256=qear5ErW3smLDATxtnUsgkX3f177zsn65ixVW8T0JVE,1641
244
244
  unique_toolkit/app/chat_event_filter_options_settings.py,sha256=W0Ydio1UeED5_xqaTzbcGzK2tH4EpA1WHUzdF-iWpqI,364
@@ -392,7 +392,7 @@ unique_toolkit/language_model/builder.py,sha256=KwBzsD6-fvQnB4p2I7M-5Tk6j4tmQ973
392
392
  unique_toolkit/language_model/constants.py,sha256=B-topqW0r83dkC_25DeQfnPk3n53qzIHUCBS7YJ0-1U,119
393
393
  unique_toolkit/language_model/default_language_model.py,sha256=mcyuFs_V6tp52C8xJSHiEHO4kFGj6xed1gvozvCRD7k,1181
394
394
  unique_toolkit/language_model/functions.py,sha256=uYIsVQfF4uwW6cftlKhQElMLpXNaVe_57FFobhC7R4A,23503
395
- unique_toolkit/language_model/infos.py,sha256=1m7B6ClIDSD9T_fo_cATFiNGkWGbPG1aFdlDSldQVx0,143054
395
+ unique_toolkit/language_model/infos.py,sha256=oAAQYhC87X02IxQKAh9IfzNsrwqL-gWzpl17m8BrHP0,145541
396
396
  unique_toolkit/language_model/invocation_stats.py,sha256=XAM_qckI-BoMBSgDpVcxFD1cFSdi1-GHZToHz_jLynM,2491
397
397
  unique_toolkit/language_model/model_costs.py,sha256=WM1u2fWAKpPB_creS-ew8YAzc1V8oiaCbvhpSJaO8B0,3789
398
398
  unique_toolkit/language_model/prompt.py,sha256=JSawaLjQg3VR-E2fK8engFyJnNdk21zaO8pPIodzN4Q,3991
@@ -419,6 +419,6 @@ unique_toolkit/short_term_memory/service.py,sha256=2JMBEBn6GoyK-a0xvdcWpPft-3C74
419
419
  unique_toolkit/smart_rules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
420
420
  unique_toolkit/smart_rules/compile.py,sha256=vRe0aMJWuVIK-VsqomgxQImzke7MDmwgSxxJfYCa7zw,1386
421
421
  unique_toolkit/test_utilities/events.py,sha256=FLrhMh0A_mQutAUjoD0FdMcVQ4Gq31r35OVGzRjvLro,6760
422
- unique_toolkit-2026.34.0.dev0.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
423
- unique_toolkit-2026.34.0.dev0.dist-info/METADATA,sha256=Lyc4K9HylzXIg01YxNdOQQPeiub11RlYO3_5HkDergo,7494
424
- unique_toolkit-2026.34.0.dev0.dist-info/RECORD,,
422
+ unique_toolkit-2026.34.0.dev2.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
423
+ unique_toolkit-2026.34.0.dev2.dist-info/METADATA,sha256=sSnbBxt3xMhYRWjORpXENMgfFSyVIc6ibCmenVbxoco,7494
424
+ unique_toolkit-2026.34.0.dev2.dist-info/RECORD,,