cat-stack 2.0.0b6__tar.gz → 2.1.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.
- {cat_stack-2.0.0b6 → cat_stack-2.1.0}/PKG-INFO +3 -1
- {cat_stack-2.0.0b6 → cat_stack-2.1.0}/pyproject.toml +1 -0
- {cat_stack-2.0.0b6 → cat_stack-2.1.0}/src/catstack/__about__.py +1 -1
- {cat_stack-2.0.0b6 → cat_stack-2.1.0}/src/catstack/_providers.py +60 -1
- {cat_stack-2.0.0b6 → cat_stack-2.1.0}/src/catstack/_utils.py +38 -0
- {cat_stack-2.0.0b6 → cat_stack-2.1.0}/src/catstack/classify.py +111 -15
- {cat_stack-2.0.0b6 → cat_stack-2.1.0}/src/catstack/prompt_tune.py +9 -13
- {cat_stack-2.0.0b6 → cat_stack-2.1.0}/src/catstack/text_functions.py +3 -3
- {cat_stack-2.0.0b6 → cat_stack-2.1.0}/src/catstack/text_functions_ensemble.py +13 -1
- {cat_stack-2.0.0b6 → cat_stack-2.1.0}/.gitignore +0 -0
- {cat_stack-2.0.0b6 → cat_stack-2.1.0}/LICENSE +0 -0
- {cat_stack-2.0.0b6 → cat_stack-2.1.0}/README.md +0 -0
- {cat_stack-2.0.0b6 → cat_stack-2.1.0}/src/cat_stack/__init__.py +0 -0
- {cat_stack-2.0.0b6 → cat_stack-2.1.0}/src/catstack/__init__.py +0 -0
- {cat_stack-2.0.0b6 → cat_stack-2.1.0}/src/catstack/_batch.py +0 -0
- {cat_stack-2.0.0b6 → cat_stack-2.1.0}/src/catstack/_category_analysis.py +0 -0
- {cat_stack-2.0.0b6 → cat_stack-2.1.0}/src/catstack/_chunked.py +0 -0
- {cat_stack-2.0.0b6 → cat_stack-2.1.0}/src/catstack/_embeddings.py +0 -0
- {cat_stack-2.0.0b6 → cat_stack-2.1.0}/src/catstack/_formatter.py +0 -0
- {cat_stack-2.0.0b6 → cat_stack-2.1.0}/src/catstack/_pilot_test.py +0 -0
- {cat_stack-2.0.0b6 → cat_stack-2.1.0}/src/catstack/_prompts.py +0 -0
- {cat_stack-2.0.0b6 → cat_stack-2.1.0}/src/catstack/_review_ui.py +0 -0
- {cat_stack-2.0.0b6 → cat_stack-2.1.0}/src/catstack/_tiebreaker.py +0 -0
- {cat_stack-2.0.0b6 → cat_stack-2.1.0}/src/catstack/_web_fetch.py +0 -0
- {cat_stack-2.0.0b6 → cat_stack-2.1.0}/src/catstack/_wrapper_helpers.py +0 -0
- {cat_stack-2.0.0b6 → cat_stack-2.1.0}/src/catstack/calls/CoVe.py +0 -0
- {cat_stack-2.0.0b6 → cat_stack-2.1.0}/src/catstack/calls/__init__.py +0 -0
- {cat_stack-2.0.0b6 → cat_stack-2.1.0}/src/catstack/calls/image_CoVe.py +0 -0
- {cat_stack-2.0.0b6 → cat_stack-2.1.0}/src/catstack/calls/image_stepback.py +0 -0
- {cat_stack-2.0.0b6 → cat_stack-2.1.0}/src/catstack/calls/pdf_CoVe.py +0 -0
- {cat_stack-2.0.0b6 → cat_stack-2.1.0}/src/catstack/calls/pdf_stepback.py +0 -0
- {cat_stack-2.0.0b6 → cat_stack-2.1.0}/src/catstack/calls/stepback.py +0 -0
- {cat_stack-2.0.0b6 → cat_stack-2.1.0}/src/catstack/calls/top_n.py +0 -0
- {cat_stack-2.0.0b6 → cat_stack-2.1.0}/src/catstack/collapse_themes.py +0 -0
- {cat_stack-2.0.0b6 → cat_stack-2.1.0}/src/catstack/explore.py +0 -0
- {cat_stack-2.0.0b6 → cat_stack-2.1.0}/src/catstack/extract.py +0 -0
- {cat_stack-2.0.0b6 → cat_stack-2.1.0}/src/catstack/image_functions.py +0 -0
- {cat_stack-2.0.0b6 → cat_stack-2.1.0}/src/catstack/images/circle.png +0 -0
- {cat_stack-2.0.0b6 → cat_stack-2.1.0}/src/catstack/images/cube.png +0 -0
- {cat_stack-2.0.0b6 → cat_stack-2.1.0}/src/catstack/images/diamond.png +0 -0
- {cat_stack-2.0.0b6 → cat_stack-2.1.0}/src/catstack/images/overlapping_pentagons.png +0 -0
- {cat_stack-2.0.0b6 → cat_stack-2.1.0}/src/catstack/images/rectangles.png +0 -0
- {cat_stack-2.0.0b6 → cat_stack-2.1.0}/src/catstack/model_reference_list.py +0 -0
- {cat_stack-2.0.0b6 → cat_stack-2.1.0}/src/catstack/pdf_functions.py +0 -0
- {cat_stack-2.0.0b6 → cat_stack-2.1.0}/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
|
|
3
|
+
Version: 2.1.0
|
|
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
|
|
4
|
+
__version__ = "2.1.0"
|
|
5
5
|
__author__ = "Chris Soria"
|
|
6
6
|
__email__ = "chrissoria@berkeley.edu"
|
|
7
7
|
__title__ = "cat-stack"
|
|
@@ -721,6 +721,11 @@ 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
|
|
|
@@ -1195,7 +1200,7 @@ class UnifiedLLMClient:
|
|
|
1195
1200
|
except FileNotFoundError:
|
|
1196
1201
|
return None, (
|
|
1197
1202
|
"Claude CLI not found. Install it: "
|
|
1198
|
-
"https://
|
|
1203
|
+
"https://code.claude.com/docs"
|
|
1199
1204
|
)
|
|
1200
1205
|
|
|
1201
1206
|
return None, "Max retries exceeded"
|
|
@@ -1206,6 +1211,55 @@ class UnifiedLLMClient:
|
|
|
1206
1211
|
# contract that callers depend on.
|
|
1207
1212
|
return None, f"Claude CLI subprocess failed: {e} (prompt may be too large for argv)"
|
|
1208
1213
|
|
|
1214
|
+
def _call_claude_agent(
|
|
1215
|
+
self,
|
|
1216
|
+
messages: list,
|
|
1217
|
+
thinking_budget: int = None,
|
|
1218
|
+
) -> tuple[str, str | None]:
|
|
1219
|
+
"""Route one completion through the cat-claws SDK adapter.
|
|
1220
|
+
|
|
1221
|
+
Like `_call_claude_cli`, this runs on the user's Claude subscription
|
|
1222
|
+
(no API key) and returns the same (text, error) contract. cat-claws is
|
|
1223
|
+
an optional dependency (the `[agent]` extra); a missing install
|
|
1224
|
+
degrades to a clear install hint rather than an ImportError traceback.
|
|
1225
|
+
|
|
1226
|
+
The adapter is async. complete() is sync and may run inside ensemble
|
|
1227
|
+
worker threads, so we drive one sealed call per invocation with
|
|
1228
|
+
asyncio.run (a fresh loop per call) - never a shared/module-global
|
|
1229
|
+
loop. Message flattening mirrors _call_claude_cli exactly.
|
|
1230
|
+
"""
|
|
1231
|
+
try:
|
|
1232
|
+
from catclaws._adapters import get_adapter
|
|
1233
|
+
except ImportError:
|
|
1234
|
+
return None, (
|
|
1235
|
+
"cat-claws is not installed. Install it to use "
|
|
1236
|
+
"model_source='claude-agent': pip install cat-stack[agent]"
|
|
1237
|
+
)
|
|
1238
|
+
import asyncio
|
|
1239
|
+
|
|
1240
|
+
system_parts = []
|
|
1241
|
+
user_parts = []
|
|
1242
|
+
for msg in messages:
|
|
1243
|
+
if msg["role"] == "system":
|
|
1244
|
+
system_parts.append(msg["content"])
|
|
1245
|
+
elif msg["role"] in ("user", "assistant"):
|
|
1246
|
+
user_parts.append(msg["content"])
|
|
1247
|
+
system_prompt = "\n\n".join(system_parts) if system_parts else None
|
|
1248
|
+
user_prompt = "\n\n".join(user_parts)
|
|
1249
|
+
|
|
1250
|
+
adapter = get_adapter("claude")
|
|
1251
|
+
try:
|
|
1252
|
+
return asyncio.run(
|
|
1253
|
+
adapter.one_shot(
|
|
1254
|
+
user_prompt,
|
|
1255
|
+
system_prompt=system_prompt,
|
|
1256
|
+
model=self.model,
|
|
1257
|
+
thinking_budget=thinking_budget or 0,
|
|
1258
|
+
)
|
|
1259
|
+
)
|
|
1260
|
+
except Exception as e:
|
|
1261
|
+
return None, f"cat-claws call failed: {e}"
|
|
1262
|
+
|
|
1209
1263
|
def complete(
|
|
1210
1264
|
self,
|
|
1211
1265
|
messages: list,
|
|
@@ -1249,6 +1303,9 @@ class UnifiedLLMClient:
|
|
|
1249
1303
|
if self.provider == "claude-code":
|
|
1250
1304
|
return self._call_claude_cli(messages, max_retries=max_retries, initial_delay=initial_delay)
|
|
1251
1305
|
|
|
1306
|
+
if self.provider == "claude-agent":
|
|
1307
|
+
return self._call_claude_agent(messages, thinking_budget=thinking_budget)
|
|
1308
|
+
|
|
1252
1309
|
headers = self._get_headers()
|
|
1253
1310
|
payload = self._build_payload(messages, json_schema, creativity, thinking_budget=thinking_budget, force_json=force_json)
|
|
1254
1311
|
|
|
@@ -1741,6 +1798,8 @@ def _detect_model_source(user_model, model_source):
|
|
|
1741
1798
|
still use this name. Will be inlined in a future cleanup."""
|
|
1742
1799
|
if model_source and model_source.lower() == "claude-code":
|
|
1743
1800
|
return "claude-code"
|
|
1801
|
+
if model_source and model_source.lower() == "claude-agent":
|
|
1802
|
+
return "claude-agent"
|
|
1744
1803
|
return detect_provider(user_model, provider=model_source)
|
|
1745
1804
|
|
|
1746
1805
|
|
|
@@ -9,6 +9,8 @@ import json
|
|
|
9
9
|
import re
|
|
10
10
|
|
|
11
11
|
__all__ = [
|
|
12
|
+
# Param resolution
|
|
13
|
+
"_resolve_description_context",
|
|
12
14
|
# JSON utilities
|
|
13
15
|
"build_json_schema",
|
|
14
16
|
"validate_classification_json",
|
|
@@ -31,6 +33,42 @@ __all__ = [
|
|
|
31
33
|
]
|
|
32
34
|
|
|
33
35
|
|
|
36
|
+
# =============================================================================
|
|
37
|
+
# Param Resolution
|
|
38
|
+
# =============================================================================
|
|
39
|
+
|
|
40
|
+
def _resolve_description_context(description, survey_question, fn_name):
|
|
41
|
+
"""Reconcile the canonical `description=` with its deprecated alias
|
|
42
|
+
`survey_question=` for entry points whose downstream prompt assembly
|
|
43
|
+
still keys the text-prompt "Context:" line (plus step-back and
|
|
44
|
+
categories="auto") off `survey_question`.
|
|
45
|
+
|
|
46
|
+
Returns the reconciled ``(description, survey_question)`` pair:
|
|
47
|
+
- only survey_question given -> DeprecationWarning; mirrored into
|
|
48
|
+
description.
|
|
49
|
+
- only description given -> mirrored into survey_question so the context
|
|
50
|
+
framing isn't silently lost (description-only callers include every
|
|
51
|
+
domain wrapper).
|
|
52
|
+
- both given -> kept distinct (e.g. cat-vader: survey_question= feed
|
|
53
|
+
question for the Context line, description= platform context).
|
|
54
|
+
"""
|
|
55
|
+
import warnings
|
|
56
|
+
|
|
57
|
+
if survey_question:
|
|
58
|
+
warnings.warn(
|
|
59
|
+
f"`survey_question=` is deprecated in {fn_name}(); use "
|
|
60
|
+
"`description=` instead. The value will be mirrored to "
|
|
61
|
+
"`description` for now.",
|
|
62
|
+
DeprecationWarning,
|
|
63
|
+
stacklevel=3,
|
|
64
|
+
)
|
|
65
|
+
if not description:
|
|
66
|
+
description = survey_question
|
|
67
|
+
elif description:
|
|
68
|
+
survey_question = description
|
|
69
|
+
return description, survey_question
|
|
70
|
+
|
|
71
|
+
|
|
34
72
|
# =============================================================================
|
|
35
73
|
# Label Cleaning
|
|
36
74
|
# =============================================================================
|
|
@@ -41,6 +41,86 @@ from .image_functions import image_multi_class
|
|
|
41
41
|
from .pdf_functions import pdf_multi_class
|
|
42
42
|
|
|
43
43
|
|
|
44
|
+
# Minimum estimated API calls (rows x batch-capable models) before the
|
|
45
|
+
# batch-mode cost tip is worth printing. Below this, the absolute savings
|
|
46
|
+
# are small and the async round-trip isn't worth suggesting.
|
|
47
|
+
_BATCH_NUDGE_MIN_REQUESTS = 500
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _maybe_print_batch_nudge(
|
|
51
|
+
input_data,
|
|
52
|
+
models,
|
|
53
|
+
categories_per_call,
|
|
54
|
+
chain_of_verification,
|
|
55
|
+
embedding_tiebreaker,
|
|
56
|
+
progress_callback,
|
|
57
|
+
):
|
|
58
|
+
"""Print a one-line cost tip when a synchronous run qualifies for
|
|
59
|
+
batch_mode=True. Checks the same eligibility rules the batch path
|
|
60
|
+
enforces, so the tip is only shown when opting in would actually work."""
|
|
61
|
+
# Options the batch path rejects or ignores -> no tip.
|
|
62
|
+
if (
|
|
63
|
+
categories_per_call is not None
|
|
64
|
+
or chain_of_verification
|
|
65
|
+
or embedding_tiebreaker
|
|
66
|
+
or progress_callback is not None
|
|
67
|
+
):
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
n_rows = len(input_data)
|
|
72
|
+
except TypeError:
|
|
73
|
+
return
|
|
74
|
+
if n_rows == 0:
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
# Batch mode is text-only.
|
|
78
|
+
from .text_functions_ensemble import _detect_input_type
|
|
79
|
+
if _detect_input_type(input_data) != "text":
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
# Count models on batch-capable providers (openai/anthropic/google/
|
|
83
|
+
# mistral/xai). `models` is already normalized to a list here; provider
|
|
84
|
+
# may still be "auto"/None in the spec, so resolve it the same way
|
|
85
|
+
# prepare_model_configs will.
|
|
86
|
+
from ._batch import UNSUPPORTED_BATCH_PROVIDERS
|
|
87
|
+
from ._providers import detect_provider
|
|
88
|
+
|
|
89
|
+
n_capable = 0
|
|
90
|
+
for m in models:
|
|
91
|
+
name, provider = None, None
|
|
92
|
+
if isinstance(m, (list, tuple)):
|
|
93
|
+
name = m[0] if len(m) >= 1 else None
|
|
94
|
+
provider = m[1] if len(m) >= 2 else None
|
|
95
|
+
elif isinstance(m, dict):
|
|
96
|
+
name = m.get("model")
|
|
97
|
+
provider = m.get("provider")
|
|
98
|
+
elif isinstance(m, str):
|
|
99
|
+
name = m
|
|
100
|
+
if not provider or provider == "auto":
|
|
101
|
+
if not name:
|
|
102
|
+
continue
|
|
103
|
+
try:
|
|
104
|
+
provider = detect_provider(name)
|
|
105
|
+
except Exception:
|
|
106
|
+
continue
|
|
107
|
+
if provider not in UNSUPPORTED_BATCH_PROVIDERS:
|
|
108
|
+
n_capable += 1
|
|
109
|
+
|
|
110
|
+
est_requests = n_rows * n_capable
|
|
111
|
+
if n_capable == 0 or est_requests < _BATCH_NUDGE_MIN_REQUESTS:
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
print(
|
|
115
|
+
f"\n[CatLLM] Tip: this run (~{est_requests:,} API calls across "
|
|
116
|
+
f"{n_capable} batch-capable model(s)) qualifies for batch_mode=True.\n"
|
|
117
|
+
" The async batch API costs ~50% less with identical prompts and\n"
|
|
118
|
+
" results, and gets higher rate limits. The trade-off is latency:\n"
|
|
119
|
+
" the job completes asynchronously (typically minutes to a few\n"
|
|
120
|
+
" hours; 24h worst case). Add batch_mode=True to opt in.\n"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
44
124
|
def classify(
|
|
45
125
|
input_data,
|
|
46
126
|
categories,
|
|
@@ -168,6 +248,8 @@ def classify(
|
|
|
168
248
|
Providers without batch API (HuggingFace, Perplexity, Ollama) fall back to
|
|
169
249
|
synchronous calls and are merged in with the batch results.
|
|
170
250
|
Incompatible with: PDF/image input, progress_callback.
|
|
251
|
+
Large qualifying synchronous runs (>= ~500 estimated API calls)
|
|
252
|
+
print a one-line tip suggesting batch_mode=True.
|
|
171
253
|
batch_poll_interval (float): Seconds between batch job status checks. Default 30.
|
|
172
254
|
batch_timeout (float): Max seconds to wait for batch completion. Default 86400 (24h).
|
|
173
255
|
models (list): For multi-model mode, list of (model, provider, api_key) tuples.
|
|
@@ -355,21 +437,13 @@ def classify(
|
|
|
355
437
|
... consensus_threshold="unanimous", # or "majority", "two-thirds", or 0.75
|
|
356
438
|
... )
|
|
357
439
|
"""
|
|
358
|
-
#
|
|
359
|
-
#
|
|
360
|
-
#
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
"`survey_question=` is deprecated in classify(); use "
|
|
366
|
-
"`description=` instead. The value will be mirrored to "
|
|
367
|
-
"`description` for now.",
|
|
368
|
-
DeprecationWarning,
|
|
369
|
-
stacklevel=2,
|
|
370
|
-
)
|
|
371
|
-
if not description:
|
|
372
|
-
description = survey_question
|
|
440
|
+
# Reconcile the canonical `description=` with the deprecated
|
|
441
|
+
# `survey_question=` (each is mirrored into the other when only one is
|
|
442
|
+
# given — see _resolve_description_context for the full rules).
|
|
443
|
+
from ._utils import _resolve_description_context
|
|
444
|
+
description, survey_question = _resolve_description_context(
|
|
445
|
+
description, survey_question, "classify"
|
|
446
|
+
)
|
|
373
447
|
|
|
374
448
|
# Build models list
|
|
375
449
|
if models is None:
|
|
@@ -620,6 +694,28 @@ def classify(
|
|
|
620
694
|
print("\n\n".join(_strategy_warnings))
|
|
621
695
|
print()
|
|
622
696
|
|
|
697
|
+
# =========================================================================
|
|
698
|
+
# Batch-mode cost nudge
|
|
699
|
+
# =========================================================================
|
|
700
|
+
# One-line tip when a large synchronous run would qualify for the async
|
|
701
|
+
# batch API (~50% cheaper, higher rate limits, identical prompts and
|
|
702
|
+
# results). Fires only when batch_mode=True would actually accept this
|
|
703
|
+
# run — text input, no batch-incompatible options, at least one
|
|
704
|
+
# batch-capable provider — so the tip is never a dead end. Informational
|
|
705
|
+
# only: must never affect or abort the run.
|
|
706
|
+
if not batch_mode:
|
|
707
|
+
try:
|
|
708
|
+
_maybe_print_batch_nudge(
|
|
709
|
+
input_data=input_data,
|
|
710
|
+
models=models,
|
|
711
|
+
categories_per_call=categories_per_call,
|
|
712
|
+
chain_of_verification=chain_of_verification,
|
|
713
|
+
embedding_tiebreaker=embedding_tiebreaker,
|
|
714
|
+
progress_callback=progress_callback,
|
|
715
|
+
)
|
|
716
|
+
except Exception:
|
|
717
|
+
pass
|
|
718
|
+
|
|
623
719
|
# =========================================================================
|
|
624
720
|
# JSON formatter fallback
|
|
625
721
|
# =========================================================================
|
|
@@ -186,19 +186,15 @@ def prompt_tune(
|
|
|
186
186
|
... system_prompt=result["system_prompt"],
|
|
187
187
|
... )
|
|
188
188
|
"""
|
|
189
|
-
#
|
|
190
|
-
#
|
|
191
|
-
#
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
stacklevel=2,
|
|
199
|
-
)
|
|
200
|
-
if not description:
|
|
201
|
-
description = survey_question
|
|
189
|
+
# Reconcile the canonical `description=` with the deprecated
|
|
190
|
+
# `survey_question=` (each is mirrored into the other when only one is
|
|
191
|
+
# given — see _resolve_description_context for the full rules). Without
|
|
192
|
+
# the description->survey_question direction, description-only callers
|
|
193
|
+
# ran the whole tuning loop with no "Context:" line in the prompts.
|
|
194
|
+
from ._utils import _resolve_description_context
|
|
195
|
+
description, survey_question = _resolve_description_context(
|
|
196
|
+
description, survey_question, "prompt_tune"
|
|
197
|
+
)
|
|
202
198
|
|
|
203
199
|
# Build models list
|
|
204
200
|
if models is None:
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|