cat-stack 0.3.0__tar.gz → 1.0.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-0.3.0 → cat_stack-1.0.0}/PKG-INFO +38 -1
- {cat_stack-0.3.0 → cat_stack-1.0.0}/README.md +37 -0
- {cat_stack-0.3.0 → cat_stack-1.0.0}/src/cat_stack/__about__.py +1 -1
- {cat_stack-0.3.0 → cat_stack-1.0.0}/src/cat_stack/classify.py +46 -0
- {cat_stack-0.3.0 → cat_stack-1.0.0}/src/cat_stack/summarize.py +79 -1
- {cat_stack-0.3.0 → cat_stack-1.0.0}/src/cat_stack/text_functions_ensemble.py +8 -2
- {cat_stack-0.3.0 → cat_stack-1.0.0}/.gitignore +0 -0
- {cat_stack-0.3.0 → cat_stack-1.0.0}/LICENSE +0 -0
- {cat_stack-0.3.0 → cat_stack-1.0.0}/pyproject.toml +0 -0
- {cat_stack-0.3.0 → cat_stack-1.0.0}/src/cat_stack/__init__.py +0 -0
- {cat_stack-0.3.0 → cat_stack-1.0.0}/src/cat_stack/_batch.py +0 -0
- {cat_stack-0.3.0 → cat_stack-1.0.0}/src/cat_stack/_category_analysis.py +0 -0
- {cat_stack-0.3.0 → cat_stack-1.0.0}/src/cat_stack/_chunked.py +0 -0
- {cat_stack-0.3.0 → cat_stack-1.0.0}/src/cat_stack/_embeddings.py +0 -0
- {cat_stack-0.3.0 → cat_stack-1.0.0}/src/cat_stack/_formatter.py +0 -0
- {cat_stack-0.3.0 → cat_stack-1.0.0}/src/cat_stack/_pilot_test.py +0 -0
- {cat_stack-0.3.0 → cat_stack-1.0.0}/src/cat_stack/_providers.py +0 -0
- {cat_stack-0.3.0 → cat_stack-1.0.0}/src/cat_stack/_review_ui.py +0 -0
- {cat_stack-0.3.0 → cat_stack-1.0.0}/src/cat_stack/_tiebreaker.py +0 -0
- {cat_stack-0.3.0 → cat_stack-1.0.0}/src/cat_stack/_utils.py +0 -0
- {cat_stack-0.3.0 → cat_stack-1.0.0}/src/cat_stack/_web_fetch.py +0 -0
- {cat_stack-0.3.0 → cat_stack-1.0.0}/src/cat_stack/calls/CoVe.py +0 -0
- {cat_stack-0.3.0 → cat_stack-1.0.0}/src/cat_stack/calls/__init__.py +0 -0
- {cat_stack-0.3.0 → cat_stack-1.0.0}/src/cat_stack/calls/all_calls.py +0 -0
- {cat_stack-0.3.0 → cat_stack-1.0.0}/src/cat_stack/calls/image_CoVe.py +0 -0
- {cat_stack-0.3.0 → cat_stack-1.0.0}/src/cat_stack/calls/image_stepback.py +0 -0
- {cat_stack-0.3.0 → cat_stack-1.0.0}/src/cat_stack/calls/pdf_CoVe.py +0 -0
- {cat_stack-0.3.0 → cat_stack-1.0.0}/src/cat_stack/calls/pdf_stepback.py +0 -0
- {cat_stack-0.3.0 → cat_stack-1.0.0}/src/cat_stack/calls/stepback.py +0 -0
- {cat_stack-0.3.0 → cat_stack-1.0.0}/src/cat_stack/calls/top_n.py +0 -0
- {cat_stack-0.3.0 → cat_stack-1.0.0}/src/cat_stack/explore.py +0 -0
- {cat_stack-0.3.0 → cat_stack-1.0.0}/src/cat_stack/extract.py +0 -0
- {cat_stack-0.3.0 → cat_stack-1.0.0}/src/cat_stack/image_functions.py +0 -0
- {cat_stack-0.3.0 → cat_stack-1.0.0}/src/cat_stack/images/circle.png +0 -0
- {cat_stack-0.3.0 → cat_stack-1.0.0}/src/cat_stack/images/cube.png +0 -0
- {cat_stack-0.3.0 → cat_stack-1.0.0}/src/cat_stack/images/diamond.png +0 -0
- {cat_stack-0.3.0 → cat_stack-1.0.0}/src/cat_stack/images/overlapping_pentagons.png +0 -0
- {cat_stack-0.3.0 → cat_stack-1.0.0}/src/cat_stack/images/rectangles.png +0 -0
- {cat_stack-0.3.0 → cat_stack-1.0.0}/src/cat_stack/model_reference_list.py +0 -0
- {cat_stack-0.3.0 → cat_stack-1.0.0}/src/cat_stack/pdf_functions.py +0 -0
- {cat_stack-0.3.0 → cat_stack-1.0.0}/src/cat_stack/prompt_tune.py +0 -0
- {cat_stack-0.3.0 → cat_stack-1.0.0}/src/cat_stack/text_functions.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cat-stack
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.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
|
|
@@ -100,6 +100,41 @@ cat.classify(
|
|
|
100
100
|
)
|
|
101
101
|
```
|
|
102
102
|
|
|
103
|
+
#### Inline prompt tuning
|
|
104
|
+
|
|
105
|
+
Add `prompt_tune=True` to automatically optimize the classification prompt before the full run. A browser UI opens for you to correct a small sample, then the optimized prompt is used for all remaining items.
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
cat.classify(
|
|
109
|
+
input_data=df["text"],
|
|
110
|
+
categories=["Cat A", "Cat B", "Cat C"],
|
|
111
|
+
models=[("gpt-4o", "openai", key)],
|
|
112
|
+
prompt_tune=15, # tune on 15 random items, then classify all
|
|
113
|
+
tune_iterations=3, # max attempts per category (default 3)
|
|
114
|
+
)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### `prompt_tune()`
|
|
118
|
+
Standalone automatic prompt optimization. Iteratively refines classification prompts using user feedback — classify a sample, correct mistakes in the browser, and let the LLM generate targeted per-category instructions.
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
result = cat.prompt_tune(
|
|
122
|
+
input_data=df["text"],
|
|
123
|
+
categories=["Cat A", "Cat B", "Cat C"],
|
|
124
|
+
api_key="your-key",
|
|
125
|
+
sample_size=15,
|
|
126
|
+
max_iterations=3,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Use the optimized prompt for classification
|
|
130
|
+
cat.classify(
|
|
131
|
+
input_data=df["text"],
|
|
132
|
+
categories=["Cat A", "Cat B", "Cat C"],
|
|
133
|
+
api_key="your-key",
|
|
134
|
+
system_prompt=result["system_prompt"],
|
|
135
|
+
)
|
|
136
|
+
```
|
|
137
|
+
|
|
103
138
|
### `extract()`
|
|
104
139
|
Discover categories from a corpus using LLM-driven exploration.
|
|
105
140
|
|
|
@@ -141,11 +176,13 @@ All providers use the same `(model_name, provider, api_key)` tuple format. Provi
|
|
|
141
176
|
|
|
142
177
|
## Features
|
|
143
178
|
|
|
179
|
+
- **Automatic prompt optimization** (`prompt_tune`) — correct a small sample in a browser UI, and the system generates per-category instructions that improve accuracy
|
|
144
180
|
- **Multi-model ensemble** with consensus voting and agreement scores
|
|
145
181
|
- **Batch API support** for OpenAI, Anthropic, Google, Mistral, and xAI
|
|
146
182
|
- **Prompt strategies**: Chain-of-Thought, Chain-of-Verification, step-back prompting, few-shot examples
|
|
147
183
|
- **Text, image, and PDF** input auto-detection
|
|
148
184
|
- **Embedding similarity** tiebreaker for ensemble consensus ties
|
|
185
|
+
- **Pilot test** — validate classifications on a small sample before committing to the full run
|
|
149
186
|
|
|
150
187
|
## License
|
|
151
188
|
|
|
@@ -61,6 +61,41 @@ cat.classify(
|
|
|
61
61
|
)
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
+
#### Inline prompt tuning
|
|
65
|
+
|
|
66
|
+
Add `prompt_tune=True` to automatically optimize the classification prompt before the full run. A browser UI opens for you to correct a small sample, then the optimized prompt is used for all remaining items.
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
cat.classify(
|
|
70
|
+
input_data=df["text"],
|
|
71
|
+
categories=["Cat A", "Cat B", "Cat C"],
|
|
72
|
+
models=[("gpt-4o", "openai", key)],
|
|
73
|
+
prompt_tune=15, # tune on 15 random items, then classify all
|
|
74
|
+
tune_iterations=3, # max attempts per category (default 3)
|
|
75
|
+
)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### `prompt_tune()`
|
|
79
|
+
Standalone automatic prompt optimization. Iteratively refines classification prompts using user feedback — classify a sample, correct mistakes in the browser, and let the LLM generate targeted per-category instructions.
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
result = cat.prompt_tune(
|
|
83
|
+
input_data=df["text"],
|
|
84
|
+
categories=["Cat A", "Cat B", "Cat C"],
|
|
85
|
+
api_key="your-key",
|
|
86
|
+
sample_size=15,
|
|
87
|
+
max_iterations=3,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Use the optimized prompt for classification
|
|
91
|
+
cat.classify(
|
|
92
|
+
input_data=df["text"],
|
|
93
|
+
categories=["Cat A", "Cat B", "Cat C"],
|
|
94
|
+
api_key="your-key",
|
|
95
|
+
system_prompt=result["system_prompt"],
|
|
96
|
+
)
|
|
97
|
+
```
|
|
98
|
+
|
|
64
99
|
### `extract()`
|
|
65
100
|
Discover categories from a corpus using LLM-driven exploration.
|
|
66
101
|
|
|
@@ -102,11 +137,13 @@ All providers use the same `(model_name, provider, api_key)` tuple format. Provi
|
|
|
102
137
|
|
|
103
138
|
## Features
|
|
104
139
|
|
|
140
|
+
- **Automatic prompt optimization** (`prompt_tune`) — correct a small sample in a browser UI, and the system generates per-category instructions that improve accuracy
|
|
105
141
|
- **Multi-model ensemble** with consensus voting and agreement scores
|
|
106
142
|
- **Batch API support** for OpenAI, Anthropic, Google, Mistral, and xAI
|
|
107
143
|
- **Prompt strategies**: Chain-of-Thought, Chain-of-Verification, step-back prompting, few-shot examples
|
|
108
144
|
- **Text, image, and PDF** input auto-detection
|
|
109
145
|
- **Embedding similarity** tiebreaker for ensemble consensus ties
|
|
146
|
+
- **Pilot test** — validate classifications on a small sample before committing to the full run
|
|
110
147
|
|
|
111
148
|
## License
|
|
112
149
|
|
|
@@ -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__ = "0.
|
|
4
|
+
__version__ = "1.0.0"
|
|
5
5
|
__author__ = "Chris Soria"
|
|
6
6
|
__email__ = "chrissoria@berkeley.edu"
|
|
7
7
|
__title__ = "cat-stack"
|
|
@@ -100,6 +100,10 @@ def classify(
|
|
|
100
100
|
categories_per_call: int = None,
|
|
101
101
|
pilot_test: Union[bool, int] = False,
|
|
102
102
|
system_prompt: str = "",
|
|
103
|
+
prompt_tune: Union[bool, int] = False,
|
|
104
|
+
tune_iterations: int = 3,
|
|
105
|
+
tune_ui: str = "browser",
|
|
106
|
+
tune_optimize: str = "balanced",
|
|
103
107
|
):
|
|
104
108
|
"""
|
|
105
109
|
Unified classification function for text, image, and PDF inputs.
|
|
@@ -242,6 +246,17 @@ def classify(
|
|
|
242
246
|
classification prompt. Use prompt_tune() to generate an optimized
|
|
243
247
|
instruction from labeled examples. Takes precedence over
|
|
244
248
|
context_prompt when provided. Default "".
|
|
249
|
+
prompt_tune (bool or int): Run automatic prompt optimization before the
|
|
250
|
+
full classification. Classifies a small sample, opens a browser UI
|
|
251
|
+
for corrections, then generates an optimized system_prompt.
|
|
252
|
+
- False (default): Skip prompt tuning.
|
|
253
|
+
- True: Tune on 10 random items.
|
|
254
|
+
- int: Tune on that many random items.
|
|
255
|
+
Overrides system_prompt if provided.
|
|
256
|
+
tune_iterations (int): Max optimization attempts per category. Default 3.
|
|
257
|
+
tune_ui (str): Review UI for prompt tuning — "browser" or "terminal".
|
|
258
|
+
tune_optimize (str): Metric to optimize — "balanced", "precision",
|
|
259
|
+
or "sensitivity". Default "balanced".
|
|
245
260
|
|
|
246
261
|
Returns:
|
|
247
262
|
pd.DataFrame: Results with classification columns.
|
|
@@ -400,6 +415,37 @@ def classify(
|
|
|
400
415
|
if pilot_result is None or not pilot_result["proceed"]:
|
|
401
416
|
return None
|
|
402
417
|
|
|
418
|
+
# =========================================================================
|
|
419
|
+
# Prompt tuning — optimize system_prompt before full classification
|
|
420
|
+
# =========================================================================
|
|
421
|
+
if prompt_tune and categories and categories != "auto":
|
|
422
|
+
from .prompt_tune import prompt_tune as _prompt_tune
|
|
423
|
+
|
|
424
|
+
tune_sample_size = prompt_tune if isinstance(prompt_tune, int) else 10
|
|
425
|
+
|
|
426
|
+
tune_result = _prompt_tune(
|
|
427
|
+
input_data=input_data,
|
|
428
|
+
categories=categories,
|
|
429
|
+
models=models,
|
|
430
|
+
description=description,
|
|
431
|
+
survey_question=survey_question,
|
|
432
|
+
sample_size=tune_sample_size,
|
|
433
|
+
max_iterations=tune_iterations,
|
|
434
|
+
multi_label=multi_label,
|
|
435
|
+
creativity=creativity,
|
|
436
|
+
use_json_schema=use_json_schema,
|
|
437
|
+
consensus_threshold=consensus_threshold,
|
|
438
|
+
max_retries=max_retries,
|
|
439
|
+
input_mode=input_mode,
|
|
440
|
+
ui=tune_ui,
|
|
441
|
+
optimize=tune_optimize,
|
|
442
|
+
add_other=False, # already handled above
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
if tune_result["system_prompt"]:
|
|
446
|
+
system_prompt = tune_result["system_prompt"]
|
|
447
|
+
print(f"\n[CatLLM] Using optimized prompt from prompt_tune.\n")
|
|
448
|
+
|
|
403
449
|
# =========================================================================
|
|
404
450
|
# Validate categories_per_call
|
|
405
451
|
# =========================================================================
|
|
@@ -31,6 +31,7 @@ def summarize(
|
|
|
31
31
|
api_key: str = None,
|
|
32
32
|
description: str = "",
|
|
33
33
|
instructions: str = "",
|
|
34
|
+
format: str = "paragraph",
|
|
34
35
|
max_length: int = None,
|
|
35
36
|
focus: str = None,
|
|
36
37
|
user_model: str = "gpt-4o",
|
|
@@ -76,7 +77,15 @@ def summarize(
|
|
|
76
77
|
- PDF: directory path, single PDF path, or list of PDF paths
|
|
77
78
|
api_key (str): API key for the model provider (single-model mode)
|
|
78
79
|
description (str): Description of what the content contains (provides context)
|
|
79
|
-
instructions (str): Specific summarization instructions
|
|
80
|
+
instructions (str): Specific summarization instructions. When used with
|
|
81
|
+
format, these are appended as additional instructions. Default "".
|
|
82
|
+
format (str): Output format for the summary. Default "paragraph".
|
|
83
|
+
- "paragraph": Flowing prose summary (default)
|
|
84
|
+
- "bullets": Bullet-point list of key points
|
|
85
|
+
- "one-liner": Single-sentence summary
|
|
86
|
+
- "structured": Labeled sections (What, Who, Why, Impact)
|
|
87
|
+
- "report": Comprehensive full-page report with Overview, Background,
|
|
88
|
+
Key Provisions, Stakeholders/Impact, and Implementation sections
|
|
80
89
|
max_length (int): Maximum summary length in words
|
|
81
90
|
focus (str): What to focus on (e.g., "main arguments", "emotional content")
|
|
82
91
|
user_model (str): Model to use (default "gpt-4o")
|
|
@@ -179,6 +188,75 @@ def summarize(
|
|
|
179
188
|
... ],
|
|
180
189
|
... )
|
|
181
190
|
"""
|
|
191
|
+
# =========================================================================
|
|
192
|
+
# Resolve format → instructions + max_length defaults
|
|
193
|
+
# =========================================================================
|
|
194
|
+
_FORMAT_PRESETS = {
|
|
195
|
+
"paragraph": {
|
|
196
|
+
"instructions": "Write a concise summary in paragraph form.",
|
|
197
|
+
"max_length": None,
|
|
198
|
+
},
|
|
199
|
+
"bullets": {
|
|
200
|
+
"instructions": (
|
|
201
|
+
"Summarize as a bullet-point list. Each bullet should capture "
|
|
202
|
+
"one key point. Use '- ' prefix for each bullet."
|
|
203
|
+
),
|
|
204
|
+
"max_length": None,
|
|
205
|
+
},
|
|
206
|
+
"one-liner": {
|
|
207
|
+
"instructions": "Summarize in a single sentence.",
|
|
208
|
+
"max_length": 40,
|
|
209
|
+
},
|
|
210
|
+
"structured": {
|
|
211
|
+
"instructions": (
|
|
212
|
+
"Summarize using these labeled sections:\n"
|
|
213
|
+
"- What: What does this do or say?\n"
|
|
214
|
+
"- Who: Who is affected or involved?\n"
|
|
215
|
+
"- Why: What is the motivation or purpose?\n"
|
|
216
|
+
"- Impact: What are the key consequences or effects?"
|
|
217
|
+
),
|
|
218
|
+
"max_length": None,
|
|
219
|
+
},
|
|
220
|
+
"report": {
|
|
221
|
+
"instructions": (
|
|
222
|
+
"Write a comprehensive full-page report covering the following sections. "
|
|
223
|
+
"Use clear headings and be thorough.\n\n"
|
|
224
|
+
"## Overview\n"
|
|
225
|
+
"A brief executive summary (2-3 sentences).\n\n"
|
|
226
|
+
"## Background and Context\n"
|
|
227
|
+
"What is the background? What problem or situation prompted this? "
|
|
228
|
+
"Include relevant history and prior actions.\n\n"
|
|
229
|
+
"## Key Provisions\n"
|
|
230
|
+
"Detail the main provisions, requirements, or arguments. "
|
|
231
|
+
"Be specific about numbers, dates, names, and conditions.\n\n"
|
|
232
|
+
"## Stakeholders and Impact\n"
|
|
233
|
+
"Who is affected? What are the expected consequences? "
|
|
234
|
+
"Include both intended effects and potential concerns.\n\n"
|
|
235
|
+
"## Implementation\n"
|
|
236
|
+
"How will this be implemented? What is the timeline? "
|
|
237
|
+
"Are there enforcement mechanisms or milestones?"
|
|
238
|
+
),
|
|
239
|
+
"max_length": 800,
|
|
240
|
+
},
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
format_lower = format.lower() if format else "paragraph"
|
|
244
|
+
if format_lower not in _FORMAT_PRESETS:
|
|
245
|
+
valid = ", ".join(f'"{k}"' for k in _FORMAT_PRESETS)
|
|
246
|
+
raise ValueError(f"format must be one of {valid}, got '{format}'")
|
|
247
|
+
|
|
248
|
+
preset = _FORMAT_PRESETS[format_lower]
|
|
249
|
+
|
|
250
|
+
# Format instructions are prepended to any user-provided instructions
|
|
251
|
+
if not instructions:
|
|
252
|
+
instructions = preset["instructions"]
|
|
253
|
+
else:
|
|
254
|
+
instructions = f"{preset['instructions']}\n\nAdditional instructions: {instructions}"
|
|
255
|
+
|
|
256
|
+
# Use format's max_length as default only if user didn't specify one
|
|
257
|
+
if max_length is None and preset["max_length"] is not None:
|
|
258
|
+
max_length = preset["max_length"]
|
|
259
|
+
|
|
182
260
|
# Map mode to pdf_mode
|
|
183
261
|
pdf_mode = mode if mode in ("image", "text", "both") else "image"
|
|
184
262
|
|
|
@@ -3756,7 +3756,7 @@ def summarize_ensemble(
|
|
|
3756
3756
|
max_retries=max_retries,
|
|
3757
3757
|
)
|
|
3758
3758
|
else:
|
|
3759
|
-
response,
|
|
3759
|
+
response, error = client.complete(
|
|
3760
3760
|
messages=messages,
|
|
3761
3761
|
json_schema=json_schema,
|
|
3762
3762
|
creativity=creativity,
|
|
@@ -3764,6 +3764,9 @@ def summarize_ensemble(
|
|
|
3764
3764
|
max_retries=max_retries,
|
|
3765
3765
|
)
|
|
3766
3766
|
|
|
3767
|
+
if error:
|
|
3768
|
+
return (model_name, '{"summary": ""}', error)
|
|
3769
|
+
|
|
3767
3770
|
# Extract JSON from response
|
|
3768
3771
|
json_str = extract_json(response)
|
|
3769
3772
|
|
|
@@ -3806,7 +3809,7 @@ def summarize_ensemble(
|
|
|
3806
3809
|
# Resolve thinking_budget for this provider
|
|
3807
3810
|
effective_thinking = thinking_budget if cfg["provider"] in ("google", "openai", "anthropic", "huggingface", "huggingface-together") else None
|
|
3808
3811
|
|
|
3809
|
-
response,
|
|
3812
|
+
response, error = client.complete(
|
|
3810
3813
|
messages=messages,
|
|
3811
3814
|
json_schema=json_schema,
|
|
3812
3815
|
creativity=creativity,
|
|
@@ -3814,6 +3817,9 @@ def summarize_ensemble(
|
|
|
3814
3817
|
max_retries=max_retries,
|
|
3815
3818
|
)
|
|
3816
3819
|
|
|
3820
|
+
if error:
|
|
3821
|
+
return (model_name, '{"summary": ""}', error)
|
|
3822
|
+
|
|
3817
3823
|
# Extract JSON from response
|
|
3818
3824
|
json_str = extract_json(response)
|
|
3819
3825
|
|
|
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
|