cat-stack 2.5.3__tar.gz → 2.5.4__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.5.3 → cat_stack-2.5.4}/PKG-INFO +1 -1
  2. {cat_stack-2.5.3 → cat_stack-2.5.4}/src/catstack/__about__.py +1 -1
  3. {cat_stack-2.5.3 → cat_stack-2.5.4}/src/catstack/text_functions_ensemble.py +58 -11
  4. {cat_stack-2.5.3 → cat_stack-2.5.4}/.gitignore +0 -0
  5. {cat_stack-2.5.3 → cat_stack-2.5.4}/LICENSE +0 -0
  6. {cat_stack-2.5.3 → cat_stack-2.5.4}/README.md +0 -0
  7. {cat_stack-2.5.3 → cat_stack-2.5.4}/pyproject.toml +0 -0
  8. {cat_stack-2.5.3 → cat_stack-2.5.4}/src/cat_stack/__init__.py +0 -0
  9. {cat_stack-2.5.3 → cat_stack-2.5.4}/src/catstack/__init__.py +0 -0
  10. {cat_stack-2.5.3 → cat_stack-2.5.4}/src/catstack/_batch.py +0 -0
  11. {cat_stack-2.5.3 → cat_stack-2.5.4}/src/catstack/_category_analysis.py +0 -0
  12. {cat_stack-2.5.3 → cat_stack-2.5.4}/src/catstack/_chunked.py +0 -0
  13. {cat_stack-2.5.3 → cat_stack-2.5.4}/src/catstack/_embeddings.py +0 -0
  14. {cat_stack-2.5.3 → cat_stack-2.5.4}/src/catstack/_formatter.py +0 -0
  15. {cat_stack-2.5.3 → cat_stack-2.5.4}/src/catstack/_pilot_test.py +0 -0
  16. {cat_stack-2.5.3 → cat_stack-2.5.4}/src/catstack/_prompts.py +0 -0
  17. {cat_stack-2.5.3 → cat_stack-2.5.4}/src/catstack/_providers.py +0 -0
  18. {cat_stack-2.5.3 → cat_stack-2.5.4}/src/catstack/_review_ui.py +0 -0
  19. {cat_stack-2.5.3 → cat_stack-2.5.4}/src/catstack/_tiebreaker.py +0 -0
  20. {cat_stack-2.5.3 → cat_stack-2.5.4}/src/catstack/_utils.py +0 -0
  21. {cat_stack-2.5.3 → cat_stack-2.5.4}/src/catstack/_web_fetch.py +0 -0
  22. {cat_stack-2.5.3 → cat_stack-2.5.4}/src/catstack/_wrapper_helpers.py +0 -0
  23. {cat_stack-2.5.3 → cat_stack-2.5.4}/src/catstack/calls/CoVe.py +0 -0
  24. {cat_stack-2.5.3 → cat_stack-2.5.4}/src/catstack/calls/__init__.py +0 -0
  25. {cat_stack-2.5.3 → cat_stack-2.5.4}/src/catstack/calls/image_CoVe.py +0 -0
  26. {cat_stack-2.5.3 → cat_stack-2.5.4}/src/catstack/calls/image_stepback.py +0 -0
  27. {cat_stack-2.5.3 → cat_stack-2.5.4}/src/catstack/calls/pdf_CoVe.py +0 -0
  28. {cat_stack-2.5.3 → cat_stack-2.5.4}/src/catstack/calls/pdf_stepback.py +0 -0
  29. {cat_stack-2.5.3 → cat_stack-2.5.4}/src/catstack/calls/stepback.py +0 -0
  30. {cat_stack-2.5.3 → cat_stack-2.5.4}/src/catstack/calls/top_n.py +0 -0
  31. {cat_stack-2.5.3 → cat_stack-2.5.4}/src/catstack/classify.py +0 -0
  32. {cat_stack-2.5.3 → cat_stack-2.5.4}/src/catstack/collapse_themes.py +0 -0
  33. {cat_stack-2.5.3 → cat_stack-2.5.4}/src/catstack/explore.py +0 -0
  34. {cat_stack-2.5.3 → cat_stack-2.5.4}/src/catstack/extract.py +0 -0
  35. {cat_stack-2.5.3 → cat_stack-2.5.4}/src/catstack/image_functions.py +0 -0
  36. {cat_stack-2.5.3 → cat_stack-2.5.4}/src/catstack/images/circle.png +0 -0
  37. {cat_stack-2.5.3 → cat_stack-2.5.4}/src/catstack/images/cube.png +0 -0
  38. {cat_stack-2.5.3 → cat_stack-2.5.4}/src/catstack/images/diamond.png +0 -0
  39. {cat_stack-2.5.3 → cat_stack-2.5.4}/src/catstack/images/overlapping_pentagons.png +0 -0
  40. {cat_stack-2.5.3 → cat_stack-2.5.4}/src/catstack/images/rectangles.png +0 -0
  41. {cat_stack-2.5.3 → cat_stack-2.5.4}/src/catstack/model_reference_list.py +0 -0
  42. {cat_stack-2.5.3 → cat_stack-2.5.4}/src/catstack/pdf_functions.py +0 -0
  43. {cat_stack-2.5.3 → cat_stack-2.5.4}/src/catstack/prompt_tune.py +0 -0
  44. {cat_stack-2.5.3 → cat_stack-2.5.4}/src/catstack/summarize.py +0 -0
  45. {cat_stack-2.5.3 → cat_stack-2.5.4}/src/catstack/text_functions.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cat-stack
3
- Version: 2.5.3
3
+ Version: 2.5.4
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
@@ -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.5.3"
4
+ __version__ = "2.5.4"
5
5
  __author__ = "Chris Soria"
6
6
  __email__ = "chrissoria@berkeley.edu"
7
7
  __title__ = "cat-stack"
@@ -1490,6 +1490,53 @@ def _extract_json_for_summary(reply: str) -> str:
1490
1490
  return '{"summary": ""}'
1491
1491
 
1492
1492
 
1493
+ _FENCE_RE = re.compile(r"^\s*```(?:json)?\s*|\s*```\s*$", re.IGNORECASE)
1494
+ _THINK_RE = re.compile(r"<think>.*?</think>", re.DOTALL)
1495
+ # `"summary": "` ... last `"` ... then only closing braces / a fence
1496
+ _LENIENT_SUMMARY_RE = re.compile(r'"summary"\s*:\s*"(.*)"\s*\}*\s*(?:```)?\s*$', re.DOTALL)
1497
+
1498
+
1499
+ def _parse_summary_reply(reply) -> tuple:
1500
+ """Read a summary out of a model reply as tolerantly as is safe.
1501
+
1502
+ Returns ``(json_str, error)``: ``json_str`` is a ``{"summary": ...}``
1503
+ object and ``error`` is None, or ``error`` explains why no summary could
1504
+ be read (so the row is retried and the reason reaches `error_message`,
1505
+ instead of the row failing silently as it used to).
1506
+
1507
+ Tried in order:
1508
+ 1. strict: the first JSON object in the reply (code fences and
1509
+ <think> blocks are fine);
1510
+ 2. lenient: a `"summary": "..."` object that is not valid JSON, most
1511
+ often unescaped double quotes or raw newlines inside the text
1512
+ (common with quoted category labels or non-English text);
1513
+ 3. prose: a reply with no JSON object at all is taken as the summary
1514
+ itself (the model answered in plain text).
1515
+ """
1516
+ if reply is None or not str(reply).strip():
1517
+ return '{"summary": ""}', "the model returned an empty reply"
1518
+ text = _THINK_RE.sub("", str(reply)).strip()
1519
+
1520
+ json_str = _extract_json_for_summary(text)
1521
+ ok, _ = extract_summary_from_json(json_str)
1522
+ if ok:
1523
+ return json_str, None
1524
+
1525
+ m = _LENIENT_SUMMARY_RE.search(text)
1526
+ if m:
1527
+ value = m.group(1).replace('\\"', '"').replace("\\n", "\n").strip()
1528
+ if value:
1529
+ return json.dumps({"summary": value}, ensure_ascii=False), None
1530
+
1531
+ if "{" not in text:
1532
+ prose = _FENCE_RE.sub("", text).strip()
1533
+ if prose:
1534
+ return json.dumps({"summary": prose}, ensure_ascii=False), None
1535
+
1536
+ snippet = " ".join(text.split())[:200]
1537
+ return '{"summary": ""}', f"could not read a summary from the model's reply: {snippet!r}"
1538
+
1539
+
1493
1540
  def extract_summary_from_json(json_str: str) -> tuple:
1494
1541
  """
1495
1542
  Extract summary from JSON response.
@@ -4230,10 +4277,10 @@ def summarize_ensemble(
4230
4277
  if error:
4231
4278
  return (model_name, '{"summary": ""}', error)
4232
4279
 
4233
- # Extract JSON from response
4234
- json_str = _extract_json_for_summary(response)
4235
-
4236
- return (model_name, json_str, None)
4280
+ # Read the summary; an unreadable reply is an error (retried,
4281
+ # reported), never a silent empty summary
4282
+ json_str, parse_error = _parse_summary_reply(response)
4283
+ return (model_name, json_str, parse_error)
4237
4284
 
4238
4285
  except Exception as e:
4239
4286
  error_msg = str(e)
@@ -4294,8 +4341,8 @@ def summarize_ensemble(
4294
4341
  if error:
4295
4342
  return (model_name, '{"summary": ""}', error)
4296
4343
 
4297
- json_str = _extract_json_for_summary(response)
4298
- return (model_name, json_str, None)
4344
+ json_str, parse_error = _parse_summary_reply(response)
4345
+ return (model_name, json_str, parse_error)
4299
4346
 
4300
4347
  except Exception as e:
4301
4348
  return (model_name, '{"summary": ""}', str(e))
@@ -4346,10 +4393,10 @@ def summarize_ensemble(
4346
4393
  if error:
4347
4394
  return (model_name, '{"summary": ""}', error)
4348
4395
 
4349
- # Extract JSON from response
4350
- json_str = _extract_json_for_summary(response)
4351
-
4352
- return (model_name, json_str, None)
4396
+ # Read the summary; an unreadable reply is an error (retried,
4397
+ # reported), never a silent empty summary
4398
+ json_str, parse_error = _parse_summary_reply(response)
4399
+ return (model_name, json_str, parse_error)
4353
4400
 
4354
4401
  except Exception as e:
4355
4402
  error_msg = str(e)
@@ -4735,7 +4782,7 @@ Provide your answer in JSON format: {{"summary": "your synthesized summary"}}"""
4735
4782
  max_retries=max_retries,
4736
4783
  )
4737
4784
 
4738
- json_str = _extract_json_for_summary(response)
4785
+ json_str, _ = _parse_summary_reply(response)
4739
4786
  is_valid, summary = extract_summary_from_json(json_str)
4740
4787
 
4741
4788
  if is_valid:
File without changes
File without changes
File without changes
File without changes