cat-stack 2.0.1__tar.gz → 2.1.1__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 (45) hide show
  1. {cat_stack-2.0.1 → cat_stack-2.1.1}/PKG-INFO +3 -1
  2. {cat_stack-2.0.1 → cat_stack-2.1.1}/pyproject.toml +1 -0
  3. {cat_stack-2.0.1 → cat_stack-2.1.1}/src/catstack/__about__.py +1 -1
  4. {cat_stack-2.0.1 → cat_stack-2.1.1}/src/catstack/_batch.py +1 -1
  5. {cat_stack-2.0.1 → cat_stack-2.1.1}/src/catstack/_providers.py +78 -1
  6. {cat_stack-2.0.1 → cat_stack-2.1.1}/src/catstack/image_functions.py +2 -0
  7. {cat_stack-2.0.1 → cat_stack-2.1.1}/src/catstack/pdf_functions.py +2 -0
  8. {cat_stack-2.0.1 → cat_stack-2.1.1}/src/catstack/text_functions.py +3 -3
  9. {cat_stack-2.0.1 → cat_stack-2.1.1}/src/catstack/text_functions_ensemble.py +13 -1
  10. {cat_stack-2.0.1 → cat_stack-2.1.1}/.gitignore +0 -0
  11. {cat_stack-2.0.1 → cat_stack-2.1.1}/LICENSE +0 -0
  12. {cat_stack-2.0.1 → cat_stack-2.1.1}/README.md +0 -0
  13. {cat_stack-2.0.1 → cat_stack-2.1.1}/src/cat_stack/__init__.py +0 -0
  14. {cat_stack-2.0.1 → cat_stack-2.1.1}/src/catstack/__init__.py +0 -0
  15. {cat_stack-2.0.1 → cat_stack-2.1.1}/src/catstack/_category_analysis.py +0 -0
  16. {cat_stack-2.0.1 → cat_stack-2.1.1}/src/catstack/_chunked.py +0 -0
  17. {cat_stack-2.0.1 → cat_stack-2.1.1}/src/catstack/_embeddings.py +0 -0
  18. {cat_stack-2.0.1 → cat_stack-2.1.1}/src/catstack/_formatter.py +0 -0
  19. {cat_stack-2.0.1 → cat_stack-2.1.1}/src/catstack/_pilot_test.py +0 -0
  20. {cat_stack-2.0.1 → cat_stack-2.1.1}/src/catstack/_prompts.py +0 -0
  21. {cat_stack-2.0.1 → cat_stack-2.1.1}/src/catstack/_review_ui.py +0 -0
  22. {cat_stack-2.0.1 → cat_stack-2.1.1}/src/catstack/_tiebreaker.py +0 -0
  23. {cat_stack-2.0.1 → cat_stack-2.1.1}/src/catstack/_utils.py +0 -0
  24. {cat_stack-2.0.1 → cat_stack-2.1.1}/src/catstack/_web_fetch.py +0 -0
  25. {cat_stack-2.0.1 → cat_stack-2.1.1}/src/catstack/_wrapper_helpers.py +0 -0
  26. {cat_stack-2.0.1 → cat_stack-2.1.1}/src/catstack/calls/CoVe.py +0 -0
  27. {cat_stack-2.0.1 → cat_stack-2.1.1}/src/catstack/calls/__init__.py +0 -0
  28. {cat_stack-2.0.1 → cat_stack-2.1.1}/src/catstack/calls/image_CoVe.py +0 -0
  29. {cat_stack-2.0.1 → cat_stack-2.1.1}/src/catstack/calls/image_stepback.py +0 -0
  30. {cat_stack-2.0.1 → cat_stack-2.1.1}/src/catstack/calls/pdf_CoVe.py +0 -0
  31. {cat_stack-2.0.1 → cat_stack-2.1.1}/src/catstack/calls/pdf_stepback.py +0 -0
  32. {cat_stack-2.0.1 → cat_stack-2.1.1}/src/catstack/calls/stepback.py +0 -0
  33. {cat_stack-2.0.1 → cat_stack-2.1.1}/src/catstack/calls/top_n.py +0 -0
  34. {cat_stack-2.0.1 → cat_stack-2.1.1}/src/catstack/classify.py +0 -0
  35. {cat_stack-2.0.1 → cat_stack-2.1.1}/src/catstack/collapse_themes.py +0 -0
  36. {cat_stack-2.0.1 → cat_stack-2.1.1}/src/catstack/explore.py +0 -0
  37. {cat_stack-2.0.1 → cat_stack-2.1.1}/src/catstack/extract.py +0 -0
  38. {cat_stack-2.0.1 → cat_stack-2.1.1}/src/catstack/images/circle.png +0 -0
  39. {cat_stack-2.0.1 → cat_stack-2.1.1}/src/catstack/images/cube.png +0 -0
  40. {cat_stack-2.0.1 → cat_stack-2.1.1}/src/catstack/images/diamond.png +0 -0
  41. {cat_stack-2.0.1 → cat_stack-2.1.1}/src/catstack/images/overlapping_pentagons.png +0 -0
  42. {cat_stack-2.0.1 → cat_stack-2.1.1}/src/catstack/images/rectangles.png +0 -0
  43. {cat_stack-2.0.1 → cat_stack-2.1.1}/src/catstack/model_reference_list.py +0 -0
  44. {cat_stack-2.0.1 → cat_stack-2.1.1}/src/catstack/prompt_tune.py +0 -0
  45. {cat_stack-2.0.1 → cat_stack-2.1.1}/src/catstack/summarize.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cat-stack
3
- Version: 2.0.1
3
+ Version: 2.1.1
4
4
  Summary: Domain-agnostic text, image, PDF, and DOCX classification engine powered by LLMs
5
5
  Project-URL: Documentation, https://github.com/chrissoria/cat-stack#readme
6
6
  Project-URL: Issues, https://github.com/chrissoria/cat-stack/issues
@@ -22,6 +22,8 @@ Requires-Python: >=3.8
22
22
  Requires-Dist: pandas
23
23
  Requires-Dist: requests
24
24
  Requires-Dist: tqdm
25
+ Provides-Extra: agent
26
+ Requires-Dist: cat-claws>=0.1.0; extra == 'agent'
25
27
  Provides-Extra: docx
26
28
  Requires-Dist: python-docx>=1.0.0; extra == 'docx'
27
29
  Provides-Extra: embeddings
@@ -35,6 +35,7 @@ pdf = ["PyMuPDF>=1.23.0"]
35
35
  docx = ["python-docx>=1.0.0"]
36
36
  formatter = ["torch>=2.0.0", "transformers>=4.40.0", "accelerate>=0.27.0"]
37
37
  embeddings = ["sentence-transformers>=2.2.0"]
38
+ agent = ["cat-claws>=0.1.0"]
38
39
 
39
40
  [project.urls]
40
41
  Documentation = "https://github.com/chrissoria/cat-stack#readme"
@@ -1,7 +1,7 @@
1
1
  # SPDX-FileCopyrightText: 2025-present Christopher Soria <chrissoria@berkeley.edu>
2
2
  #
3
3
  # SPDX-License-Identifier: GPL-3.0-or-later
4
- __version__ = "2.0.1"
4
+ __version__ = "2.1.1"
5
5
  __author__ = "Chris Soria"
6
6
  __email__ = "chrissoria@berkeley.edu"
7
7
  __title__ = "cat-stack"
@@ -65,7 +65,7 @@ BATCH_ENDPOINTS = {
65
65
  },
66
66
  }
67
67
 
68
- UNSUPPORTED_BATCH_PROVIDERS = {"huggingface", "huggingface-together", "perplexity", "ollama"}
68
+ UNSUPPORTED_BATCH_PROVIDERS = {"huggingface", "huggingface-together", "perplexity", "ollama", "claude-code", "claude-agent"}
69
69
 
70
70
  # Terminal states per provider
71
71
  _TERMINAL_STATES = {
@@ -721,9 +721,32 @@ PROVIDER_CONFIG = {
721
721
  "auth_header": None,
722
722
  "auth_prefix": "",
723
723
  },
724
+ "claude-agent": {
725
+ "endpoint": None, # Uses the cat-claws SDK adapter, not HTTP
726
+ "auth_header": None,
727
+ "auth_prefix": "",
728
+ },
724
729
  }
725
730
 
726
731
 
732
+ # Providers that route through complete() with no HTTP endpoint of their own
733
+ # (Claude subscription / CLI). Features that build a direct HTTP request
734
+ # (image, PDF) can't use them — guard with a clear error, not a deep crash.
735
+ _SUBSCRIPTION_PROVIDERS = ("claude-code", "claude-agent")
736
+
737
+
738
+ def _require_http_provider(model_source, feature):
739
+ """Raise a clear error when an HTTP-only feature is used with a
740
+ subscription/CLI provider (claude-code / claude-agent)."""
741
+ if model_source in _SUBSCRIPTION_PROVIDERS:
742
+ raise ValueError(
743
+ f"{feature} is not supported with model_source='{model_source}'. "
744
+ "The Claude subscription/CLI backend supports text classification, "
745
+ "extraction, and summarization, but not " + feature.lower() + ". "
746
+ "Use an API-key provider (e.g. model_source='anthropic') instead."
747
+ )
748
+
749
+
727
750
  # =============================================================================
728
751
  # Unified API Client
729
752
  # =============================================================================
@@ -1195,7 +1218,7 @@ class UnifiedLLMClient:
1195
1218
  except FileNotFoundError:
1196
1219
  return None, (
1197
1220
  "Claude CLI not found. Install it: "
1198
- "https://docs.anthropic.com/en/docs/claude-code"
1221
+ "https://code.claude.com/docs"
1199
1222
  )
1200
1223
 
1201
1224
  return None, "Max retries exceeded"
@@ -1206,6 +1229,55 @@ class UnifiedLLMClient:
1206
1229
  # contract that callers depend on.
1207
1230
  return None, f"Claude CLI subprocess failed: {e} (prompt may be too large for argv)"
1208
1231
 
1232
+ def _call_claude_agent(
1233
+ self,
1234
+ messages: list,
1235
+ thinking_budget: int = None,
1236
+ ) -> tuple[str, str | None]:
1237
+ """Route one completion through the cat-claws SDK adapter.
1238
+
1239
+ Like `_call_claude_cli`, this runs on the user's Claude subscription
1240
+ (no API key) and returns the same (text, error) contract. cat-claws is
1241
+ an optional dependency (the `[agent]` extra); a missing install
1242
+ degrades to a clear install hint rather than an ImportError traceback.
1243
+
1244
+ The adapter is async. complete() is sync and may run inside ensemble
1245
+ worker threads, so we drive one sealed call per invocation with
1246
+ asyncio.run (a fresh loop per call) - never a shared/module-global
1247
+ loop. Message flattening mirrors _call_claude_cli exactly.
1248
+ """
1249
+ try:
1250
+ from catclaws._adapters import get_adapter
1251
+ except ImportError:
1252
+ return None, (
1253
+ "cat-claws is not installed. Install it to use "
1254
+ "model_source='claude-agent': pip install cat-stack[agent]"
1255
+ )
1256
+ import asyncio
1257
+
1258
+ system_parts = []
1259
+ user_parts = []
1260
+ for msg in messages:
1261
+ if msg["role"] == "system":
1262
+ system_parts.append(msg["content"])
1263
+ elif msg["role"] in ("user", "assistant"):
1264
+ user_parts.append(msg["content"])
1265
+ system_prompt = "\n\n".join(system_parts) if system_parts else None
1266
+ user_prompt = "\n\n".join(user_parts)
1267
+
1268
+ adapter = get_adapter("claude")
1269
+ try:
1270
+ return asyncio.run(
1271
+ adapter.one_shot(
1272
+ user_prompt,
1273
+ system_prompt=system_prompt,
1274
+ model=self.model,
1275
+ thinking_budget=thinking_budget or 0,
1276
+ )
1277
+ )
1278
+ except Exception as e:
1279
+ return None, f"cat-claws call failed: {e}"
1280
+
1209
1281
  def complete(
1210
1282
  self,
1211
1283
  messages: list,
@@ -1249,6 +1321,9 @@ class UnifiedLLMClient:
1249
1321
  if self.provider == "claude-code":
1250
1322
  return self._call_claude_cli(messages, max_retries=max_retries, initial_delay=initial_delay)
1251
1323
 
1324
+ if self.provider == "claude-agent":
1325
+ return self._call_claude_agent(messages, thinking_budget=thinking_budget)
1326
+
1252
1327
  headers = self._get_headers()
1253
1328
  payload = self._build_payload(messages, json_schema, creativity, thinking_budget=thinking_budget, force_json=force_json)
1254
1329
 
@@ -1741,6 +1816,8 @@ def _detect_model_source(user_model, model_source):
1741
1816
  still use this name. Will be inlined in a future cleanup."""
1742
1817
  if model_source and model_source.lower() == "claude-code":
1743
1818
  return "claude-code"
1819
+ if model_source and model_source.lower() == "claude-agent":
1820
+ return "claude-agent"
1744
1821
  return detect_provider(user_model, provider=model_source)
1745
1822
 
1746
1823
 
@@ -1,6 +1,7 @@
1
1
  import warnings
2
2
 
3
3
  from .text_functions import _detect_model_source
4
+ from ._providers import _require_http_provider
4
5
  from ._utils import _clean_label, _extract_balanced_json
5
6
  from .calls.image_stepback import get_image_stepback_insight
6
7
 
@@ -147,6 +148,7 @@ def image_multi_class(
147
148
  raise FileNotFoundError(f"Directory {save_directory} doesn't exist")
148
149
 
149
150
  model_source = _detect_model_source(user_model, model_source)
151
+ _require_http_provider(model_source, "Image classification")
150
152
 
151
153
  image_files = _load_image_files(image_input)
152
154
 
@@ -1,6 +1,7 @@
1
1
  import warnings
2
2
 
3
3
  from .text_functions import _detect_model_source
4
+ from ._providers import _require_http_provider
4
5
  from ._utils import _clean_label, _extract_balanced_json
5
6
  from .calls.pdf_stepback import get_pdf_stepback_insight
6
7
 
@@ -392,6 +393,7 @@ def pdf_multi_class(
392
393
  raise ValueError(f"mode must be 'image', 'text', or 'both', got: {mode}")
393
394
 
394
395
  model_source = _detect_model_source(user_model, model_source)
396
+ _require_http_provider(model_source, "PDF classification")
395
397
 
396
398
  # Providers with native PDF support (only used in image/both modes)
397
399
  native_pdf_providers = {"anthropic", "google"}
@@ -410,7 +410,7 @@ def explore_corpus(
410
410
  provider = detect_provider(model, provider)
411
411
 
412
412
  # Validate api_key
413
- if provider not in ("ollama", "claude-code") and not api_key:
413
+ if provider not in ("ollama", "claude-code", "claude-agent") and not api_key:
414
414
  raise ValueError(f"api_key is required for provider '{provider}'")
415
415
 
416
416
  print(f"Exploring categories for question: '{survey_question}'")
@@ -596,7 +596,7 @@ def explore_common_categories(
596
596
  provider = detect_provider(model, provider)
597
597
 
598
598
  # Validate api_key
599
- if provider not in ("ollama", "claude-code") and not api_key:
599
+ if provider not in ("ollama", "claude-code", "claude-agent") and not api_key:
600
600
  raise ValueError(f"api_key is required for provider '{provider}'")
601
601
 
602
602
  # Ollama-specific checks
@@ -1062,7 +1062,7 @@ def multi_class(
1062
1062
  provider = detect_provider(model, provider)
1063
1063
 
1064
1064
  # Validate api_key requirement
1065
- if provider not in ("ollama", "claude-code") and not api_key:
1065
+ if provider not in ("ollama", "claude-code", "claude-agent") and not api_key:
1066
1066
  raise ValueError(f"api_key is required for provider '{provider}'")
1067
1067
 
1068
1068
  # Handle categories="auto" - auto-detect categories from the data
@@ -660,6 +660,18 @@ def prepare_model_configs(
660
660
  "Install: https://docs.anthropic.com/en/docs/claude-code\n"
661
661
  + "="*60
662
662
  )
663
+ elif detected_provider == "claude-agent":
664
+ try:
665
+ import catclaws # noqa: F401
666
+ except ImportError:
667
+ raise ConnectionError(
668
+ "\n" + "="*60 + "\n"
669
+ " CAT-AGENT NOT INSTALLED\n"
670
+ "="*60 + "\n\n"
671
+ "The cat-claws package is required to use claude-agent as a provider.\n"
672
+ "Install: pip install cat-stack[agent]\n"
673
+ + "="*60
674
+ )
663
675
  else:
664
676
  # Validate API key exists for cloud providers
665
677
  if not api_key:
@@ -670,7 +682,7 @@ def prepare_model_configs(
670
682
  # Preflight probe: test the model with a minimal JSON call to catch
671
683
  # issues (model not found, structured output not supported) before
672
684
  # processing thousands of rows.
673
- if detected_provider not in ("ollama", "claude-code"):
685
+ if detected_provider not in ("ollama", "claude-code", "claude-agent"):
674
686
  try:
675
687
  probe_client = UnifiedLLMClient(
676
688
  provider=detected_provider,
File without changes
File without changes
File without changes