ollama-classifier 0.1.0__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.
- ollama_classifier/__init__.py +30 -0
- ollama_classifier/classifier.py +845 -0
- ollama_classifier/prompts.py +97 -0
- ollama_classifier/py.typed +0 -0
- ollama_classifier/types.py +24 -0
- ollama_classifier-0.1.0.dist-info/METADATA +247 -0
- ollama_classifier-0.1.0.dist-info/RECORD +8 -0
- ollama_classifier-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Ollama Classifier - A wrapper around Ollama Python SDK for text classification.
|
|
2
|
+
|
|
3
|
+
This package provides a classifier for text classification with constrained output
|
|
4
|
+
and confidence scoring using the Ollama Python SDK.
|
|
5
|
+
|
|
6
|
+
Example usage:
|
|
7
|
+
from ollama import Client
|
|
8
|
+
from ollama_classifier import OllamaClassifier, ClassificationResult
|
|
9
|
+
|
|
10
|
+
client = Client()
|
|
11
|
+
classifier = OllamaClassifier(client, model="llama3.2")
|
|
12
|
+
|
|
13
|
+
result = classifier.classify(
|
|
14
|
+
text="I love this product!",
|
|
15
|
+
choices=["positive", "negative", "neutral"]
|
|
16
|
+
)
|
|
17
|
+
print(f"Prediction: {result.prediction}")
|
|
18
|
+
print(f"Confidence: {result.confidence:.2%}")
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from .types import ClassificationResult, ChoicesType
|
|
22
|
+
from .classifier import OllamaClassifier
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"OllamaClassifier",
|
|
26
|
+
"ClassificationResult",
|
|
27
|
+
"ChoicesType",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,845 @@
|
|
|
1
|
+
"""Main classifier module for ollama-classifier."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import math
|
|
5
|
+
from typing import Union, List, Dict, Any
|
|
6
|
+
|
|
7
|
+
from ollama import Client, AsyncClient
|
|
8
|
+
|
|
9
|
+
from .types import ClassificationResult, ChoicesType
|
|
10
|
+
from .prompts import (
|
|
11
|
+
build_classification_prompt,
|
|
12
|
+
get_choice_labels,
|
|
13
|
+
build_json_schema_for_choices,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class OllamaClassifier:
|
|
18
|
+
"""A classifier wrapper around Ollama client for text classification.
|
|
19
|
+
|
|
20
|
+
This class provides methods for classifying text into a set of predefined choices
|
|
21
|
+
with support for both fast (single-call) and complete (multi-call) scoring methods.
|
|
22
|
+
|
|
23
|
+
Attributes:
|
|
24
|
+
_client: The Ollama client (sync or async).
|
|
25
|
+
_model: The model name to use for classification.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, client: Union[Client, AsyncClient], model: str):
|
|
29
|
+
"""Initialize the classifier.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
client: An initialized Ollama Client or AsyncClient instance.
|
|
33
|
+
model: The model name to use for classification (e.g., "llama3.2").
|
|
34
|
+
"""
|
|
35
|
+
self._client = client
|
|
36
|
+
self._model = model
|
|
37
|
+
|
|
38
|
+
# =========================================================================
|
|
39
|
+
# Sync Methods - Generate
|
|
40
|
+
# =========================================================================
|
|
41
|
+
|
|
42
|
+
def generate(
|
|
43
|
+
self,
|
|
44
|
+
text: str,
|
|
45
|
+
choices: ChoicesType,
|
|
46
|
+
system_prompt: str | None = None,
|
|
47
|
+
) -> str:
|
|
48
|
+
"""Generate a constrained classification for a single text.
|
|
49
|
+
|
|
50
|
+
Uses JSON schema with enum constraint to ensure only valid choices are generated.
|
|
51
|
+
This is the fastest method as it only makes one API call and doesn't compute
|
|
52
|
+
confidence scores.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
text: The text to classify.
|
|
56
|
+
choices: Either a list of choice labels, or a dict mapping labels to descriptions.
|
|
57
|
+
system_prompt: Optional custom system prompt.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
The predicted choice label.
|
|
61
|
+
"""
|
|
62
|
+
labels = get_choice_labels(choices)
|
|
63
|
+
system, user = build_classification_prompt(text, choices, system_prompt)
|
|
64
|
+
schema = build_json_schema_for_choices(labels)
|
|
65
|
+
|
|
66
|
+
messages = [
|
|
67
|
+
{"role": "system", "content": system},
|
|
68
|
+
{"role": "user", "content": user},
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
response = self._client.chat(
|
|
72
|
+
model=self._model,
|
|
73
|
+
messages=messages,
|
|
74
|
+
format=schema,
|
|
75
|
+
options={"temperature": 0.0},
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
content = response.get("message", {}).get("content", "{}")
|
|
79
|
+
result = json.loads(content)
|
|
80
|
+
return result.get("label", "")
|
|
81
|
+
|
|
82
|
+
def batch_generate(
|
|
83
|
+
self,
|
|
84
|
+
texts: List[str],
|
|
85
|
+
choices: ChoicesType,
|
|
86
|
+
system_prompt: str | None = None,
|
|
87
|
+
) -> List[str]:
|
|
88
|
+
"""Generate constrained classifications for multiple texts.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
texts: List of texts to classify.
|
|
92
|
+
choices: Either a list of choice labels, or a dict mapping labels to descriptions.
|
|
93
|
+
system_prompt: Optional custom system prompt.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
List of predicted choice labels, one per input text.
|
|
97
|
+
"""
|
|
98
|
+
return [self.generate(text, choices, system_prompt) for text in texts]
|
|
99
|
+
|
|
100
|
+
# =========================================================================
|
|
101
|
+
# Sync Methods - Score Fast (Single-call logprob extraction)
|
|
102
|
+
# =========================================================================
|
|
103
|
+
|
|
104
|
+
def score_fast(
|
|
105
|
+
self,
|
|
106
|
+
text: str,
|
|
107
|
+
choices: ChoicesType,
|
|
108
|
+
system_prompt: str | None = None,
|
|
109
|
+
) -> ClassificationResult:
|
|
110
|
+
"""Score a classification using single-call logprob extraction.
|
|
111
|
+
|
|
112
|
+
Makes one API call with logprobs enabled and extracts the probability
|
|
113
|
+
distribution from the token distribution at the decision point.
|
|
114
|
+
This is faster but may be less reliable for capturing all choice probabilities.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
text: The text to classify.
|
|
118
|
+
choices: Either a list of choice labels, or a dict mapping labels to descriptions.
|
|
119
|
+
system_prompt: Optional custom system prompt.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
ClassificationResult with prediction, confidence, and probabilities.
|
|
123
|
+
"""
|
|
124
|
+
labels = get_choice_labels(choices)
|
|
125
|
+
system, user = build_classification_prompt(text, choices, system_prompt)
|
|
126
|
+
schema = build_json_schema_for_choices(labels)
|
|
127
|
+
|
|
128
|
+
messages = [
|
|
129
|
+
{"role": "system", "content": system},
|
|
130
|
+
{"role": "user", "content": user},
|
|
131
|
+
]
|
|
132
|
+
|
|
133
|
+
response = self._client.chat(
|
|
134
|
+
model=self._model,
|
|
135
|
+
messages=messages,
|
|
136
|
+
format=schema,
|
|
137
|
+
logprobs=True,
|
|
138
|
+
top_logprobs=20,
|
|
139
|
+
options={"temperature": 0.0},
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# Extract prediction from response
|
|
143
|
+
content = response.get("message", {}).get("content", "{}")
|
|
144
|
+
result = json.loads(content)
|
|
145
|
+
prediction = result.get("label", "")
|
|
146
|
+
|
|
147
|
+
# Extract logprobs from token distribution
|
|
148
|
+
probabilities = self._extract_probabilities_from_logprobs(
|
|
149
|
+
response, labels
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
return ClassificationResult(
|
|
153
|
+
prediction=prediction,
|
|
154
|
+
confidence=probabilities.get(prediction, 0.0),
|
|
155
|
+
probabilities=probabilities,
|
|
156
|
+
raw_response=response,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
def batch_score_fast(
|
|
160
|
+
self,
|
|
161
|
+
texts: List[str],
|
|
162
|
+
choices: ChoicesType,
|
|
163
|
+
system_prompt: str | None = None,
|
|
164
|
+
) -> List[ClassificationResult]:
|
|
165
|
+
"""Score multiple texts using fast single-call method.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
texts: List of texts to classify.
|
|
169
|
+
choices: Either a list of choice labels, or a dict mapping labels to descriptions.
|
|
170
|
+
system_prompt: Optional custom system prompt.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
List of ClassificationResults, one per input text.
|
|
174
|
+
"""
|
|
175
|
+
return [self.score_fast(text, choices, system_prompt) for text in texts]
|
|
176
|
+
|
|
177
|
+
# =========================================================================
|
|
178
|
+
# Sync Methods - Score Complete (Multi-call evaluation)
|
|
179
|
+
# =========================================================================
|
|
180
|
+
|
|
181
|
+
def score_complete(
|
|
182
|
+
self,
|
|
183
|
+
text: str,
|
|
184
|
+
choices: ChoicesType,
|
|
185
|
+
system_prompt: str | None = None,
|
|
186
|
+
) -> ClassificationResult:
|
|
187
|
+
"""Score a classification using multi-call evaluation with softmax.
|
|
188
|
+
|
|
189
|
+
Makes separate API calls for each choice to compute log P(choice|context),
|
|
190
|
+
then applies softmax for calibrated probabilities.
|
|
191
|
+
This is more accurate but slower (N API calls for N choices).
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
text: The text to classify.
|
|
195
|
+
choices: Either a list of choice labels, or a dict mapping labels to descriptions.
|
|
196
|
+
system_prompt: Optional custom system prompt.
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
ClassificationResult with prediction, confidence, and probabilities.
|
|
200
|
+
"""
|
|
201
|
+
labels = get_choice_labels(choices)
|
|
202
|
+
system, user = build_classification_prompt(text, choices, system_prompt)
|
|
203
|
+
|
|
204
|
+
# Compute logprob for each choice
|
|
205
|
+
logprobs: Dict[str, float] = {}
|
|
206
|
+
for label in labels:
|
|
207
|
+
logprobs[label] = self._get_choice_logprob(
|
|
208
|
+
system, user, label
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
# Apply softmax to get probabilities
|
|
212
|
+
probabilities = self._softmax(logprobs)
|
|
213
|
+
|
|
214
|
+
# Get prediction (highest probability)
|
|
215
|
+
prediction = max(probabilities, key=probabilities.get)
|
|
216
|
+
|
|
217
|
+
return ClassificationResult(
|
|
218
|
+
prediction=prediction,
|
|
219
|
+
confidence=probabilities[prediction],
|
|
220
|
+
probabilities=probabilities,
|
|
221
|
+
raw_response={"logprobs": logprobs},
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
def batch_score_complete(
|
|
225
|
+
self,
|
|
226
|
+
texts: List[str],
|
|
227
|
+
choices: ChoicesType,
|
|
228
|
+
system_prompt: str | None = None,
|
|
229
|
+
) -> List[ClassificationResult]:
|
|
230
|
+
"""Score multiple texts using complete multi-call method.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
texts: List of texts to classify.
|
|
234
|
+
choices: Either a list of choice labels, or a dict mapping labels to descriptions.
|
|
235
|
+
system_prompt: Optional custom system prompt.
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
List of ClassificationResults, one per input text.
|
|
239
|
+
"""
|
|
240
|
+
return [self.score_complete(text, choices, system_prompt) for text in texts]
|
|
241
|
+
|
|
242
|
+
# =========================================================================
|
|
243
|
+
# Sync Methods - Classify (Generate + Score)
|
|
244
|
+
# =========================================================================
|
|
245
|
+
|
|
246
|
+
def classify(
|
|
247
|
+
self,
|
|
248
|
+
text: str,
|
|
249
|
+
choices: ChoicesType,
|
|
250
|
+
system_prompt: str | None = None,
|
|
251
|
+
) -> ClassificationResult:
|
|
252
|
+
"""Classify text with constrained generation and fast scoring.
|
|
253
|
+
|
|
254
|
+
Combines generate() and score_fast() for a single classification
|
|
255
|
+
with confidence scores.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
text: The text to classify.
|
|
259
|
+
choices: Either a list of choice labels, or a dict mapping labels to descriptions.
|
|
260
|
+
system_prompt: Optional custom system prompt.
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
ClassificationResult with prediction, confidence, and probabilities.
|
|
264
|
+
"""
|
|
265
|
+
return self.score_fast(text, choices, system_prompt)
|
|
266
|
+
|
|
267
|
+
def classify_complete(
|
|
268
|
+
self,
|
|
269
|
+
text: str,
|
|
270
|
+
choices: ChoicesType,
|
|
271
|
+
system_prompt: str | None = None,
|
|
272
|
+
) -> ClassificationResult:
|
|
273
|
+
"""Classify text with constrained generation and complete scoring.
|
|
274
|
+
|
|
275
|
+
Combines generate() and score_complete() for accurate classification
|
|
276
|
+
with calibrated confidence scores.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
text: The text to classify.
|
|
280
|
+
choices: Either a list of choice labels, or a dict mapping labels to descriptions.
|
|
281
|
+
system_prompt: Optional custom system prompt.
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
ClassificationResult with prediction, confidence, and probabilities.
|
|
285
|
+
"""
|
|
286
|
+
# Use generate for constrained prediction
|
|
287
|
+
prediction = self.generate(text, choices, system_prompt)
|
|
288
|
+
|
|
289
|
+
# Use score_complete for probabilities
|
|
290
|
+
result = self.score_complete(text, choices, system_prompt)
|
|
291
|
+
|
|
292
|
+
# Override prediction with constrained output
|
|
293
|
+
result.prediction = prediction
|
|
294
|
+
result.confidence = result.probabilities.get(prediction, 0.0)
|
|
295
|
+
|
|
296
|
+
return result
|
|
297
|
+
|
|
298
|
+
def batch_classify(
|
|
299
|
+
self,
|
|
300
|
+
texts: List[str],
|
|
301
|
+
choices: ChoicesType,
|
|
302
|
+
system_prompt: str | None = None,
|
|
303
|
+
) -> List[ClassificationResult]:
|
|
304
|
+
"""Classify multiple texts with fast scoring.
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
texts: List of texts to classify.
|
|
308
|
+
choices: Either a list of choice labels, or a dict mapping labels to descriptions.
|
|
309
|
+
system_prompt: Optional custom system prompt.
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
List of ClassificationResults, one per input text.
|
|
313
|
+
"""
|
|
314
|
+
return [self.classify(text, choices, system_prompt) for text in texts]
|
|
315
|
+
|
|
316
|
+
def batch_classify_complete(
|
|
317
|
+
self,
|
|
318
|
+
texts: List[str],
|
|
319
|
+
choices: ChoicesType,
|
|
320
|
+
system_prompt: str | None = None,
|
|
321
|
+
) -> List[ClassificationResult]:
|
|
322
|
+
"""Classify multiple texts with complete scoring.
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
texts: List of texts to classify.
|
|
326
|
+
choices: Either a list of choice labels, or a dict mapping labels to descriptions.
|
|
327
|
+
system_prompt: Optional custom system prompt.
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
List of ClassificationResults, one per input text.
|
|
331
|
+
"""
|
|
332
|
+
return [self.classify_complete(text, choices, system_prompt) for text in texts]
|
|
333
|
+
|
|
334
|
+
# =========================================================================
|
|
335
|
+
# Async Methods - Generate
|
|
336
|
+
# =========================================================================
|
|
337
|
+
|
|
338
|
+
async def agenerate(
|
|
339
|
+
self,
|
|
340
|
+
text: str,
|
|
341
|
+
choices: ChoicesType,
|
|
342
|
+
system_prompt: str | None = None,
|
|
343
|
+
) -> str:
|
|
344
|
+
"""Async version of generate().
|
|
345
|
+
|
|
346
|
+
Generate a constrained classification for a single text.
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
text: The text to classify.
|
|
350
|
+
choices: Either a list of choice labels, or a dict mapping labels to descriptions.
|
|
351
|
+
system_prompt: Optional custom system prompt.
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
The predicted choice label.
|
|
355
|
+
"""
|
|
356
|
+
labels = get_choice_labels(choices)
|
|
357
|
+
system, user = build_classification_prompt(text, choices, system_prompt)
|
|
358
|
+
schema = build_json_schema_for_choices(labels)
|
|
359
|
+
|
|
360
|
+
messages = [
|
|
361
|
+
{"role": "system", "content": system},
|
|
362
|
+
{"role": "user", "content": user},
|
|
363
|
+
]
|
|
364
|
+
|
|
365
|
+
response = await self._client.chat(
|
|
366
|
+
model=self._model,
|
|
367
|
+
messages=messages,
|
|
368
|
+
format=schema,
|
|
369
|
+
options={"temperature": 0.0},
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
content = response.get("message", {}).get("content", "{}")
|
|
373
|
+
result = json.loads(content)
|
|
374
|
+
return result.get("label", "")
|
|
375
|
+
|
|
376
|
+
async def abatch_generate(
|
|
377
|
+
self,
|
|
378
|
+
texts: List[str],
|
|
379
|
+
choices: ChoicesType,
|
|
380
|
+
system_prompt: str | None = None,
|
|
381
|
+
) -> List[str]:
|
|
382
|
+
"""Async version of batch_generate().
|
|
383
|
+
|
|
384
|
+
Generate constrained classifications for multiple texts.
|
|
385
|
+
|
|
386
|
+
Args:
|
|
387
|
+
texts: List of texts to classify.
|
|
388
|
+
choices: Either a list of choice labels, or a dict mapping labels to descriptions.
|
|
389
|
+
system_prompt: Optional custom system prompt.
|
|
390
|
+
|
|
391
|
+
Returns:
|
|
392
|
+
List of predicted choice labels, one per input text.
|
|
393
|
+
"""
|
|
394
|
+
import asyncio
|
|
395
|
+
return await asyncio.gather(*[
|
|
396
|
+
self.agenerate(text, choices, system_prompt) for text in texts
|
|
397
|
+
])
|
|
398
|
+
|
|
399
|
+
# =========================================================================
|
|
400
|
+
# Async Methods - Score Fast
|
|
401
|
+
# =========================================================================
|
|
402
|
+
|
|
403
|
+
async def ascore_fast(
|
|
404
|
+
self,
|
|
405
|
+
text: str,
|
|
406
|
+
choices: ChoicesType,
|
|
407
|
+
system_prompt: str | None = None,
|
|
408
|
+
) -> ClassificationResult:
|
|
409
|
+
"""Async version of score_fast().
|
|
410
|
+
|
|
411
|
+
Score a classification using single-call logprob extraction.
|
|
412
|
+
|
|
413
|
+
Args:
|
|
414
|
+
text: The text to classify.
|
|
415
|
+
choices: Either a list of choice labels, or a dict mapping labels to descriptions.
|
|
416
|
+
system_prompt: Optional custom system prompt.
|
|
417
|
+
|
|
418
|
+
Returns:
|
|
419
|
+
ClassificationResult with prediction, confidence, and probabilities.
|
|
420
|
+
"""
|
|
421
|
+
labels = get_choice_labels(choices)
|
|
422
|
+
system, user = build_classification_prompt(text, choices, system_prompt)
|
|
423
|
+
schema = build_json_schema_for_choices(labels)
|
|
424
|
+
|
|
425
|
+
messages = [
|
|
426
|
+
{"role": "system", "content": system},
|
|
427
|
+
{"role": "user", "content": user},
|
|
428
|
+
]
|
|
429
|
+
|
|
430
|
+
response = await self._client.chat(
|
|
431
|
+
model=self._model,
|
|
432
|
+
messages=messages,
|
|
433
|
+
format=schema,
|
|
434
|
+
logprobs=True,
|
|
435
|
+
top_logprobs=20,
|
|
436
|
+
options={"temperature": 0.0},
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
# Extract prediction from response
|
|
440
|
+
content = response.get("message", {}).get("content", "{}")
|
|
441
|
+
result = json.loads(content)
|
|
442
|
+
prediction = result.get("label", "")
|
|
443
|
+
|
|
444
|
+
# Extract logprobs from token distribution
|
|
445
|
+
probabilities = self._extract_probabilities_from_logprobs(
|
|
446
|
+
response, labels
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
return ClassificationResult(
|
|
450
|
+
prediction=prediction,
|
|
451
|
+
confidence=probabilities.get(prediction, 0.0),
|
|
452
|
+
probabilities=probabilities,
|
|
453
|
+
raw_response=response,
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
async def abatch_score_fast(
|
|
457
|
+
self,
|
|
458
|
+
texts: List[str],
|
|
459
|
+
choices: ChoicesType,
|
|
460
|
+
system_prompt: str | None = None,
|
|
461
|
+
) -> List[ClassificationResult]:
|
|
462
|
+
"""Async version of batch_score_fast().
|
|
463
|
+
|
|
464
|
+
Score multiple texts using fast single-call method.
|
|
465
|
+
|
|
466
|
+
Args:
|
|
467
|
+
texts: List of texts to classify.
|
|
468
|
+
choices: Either a list of choice labels, or a dict mapping labels to descriptions.
|
|
469
|
+
system_prompt: Optional custom system prompt.
|
|
470
|
+
|
|
471
|
+
Returns:
|
|
472
|
+
List of ClassificationResults, one per input text.
|
|
473
|
+
"""
|
|
474
|
+
import asyncio
|
|
475
|
+
return await asyncio.gather(*[
|
|
476
|
+
self.ascore_fast(text, choices, system_prompt) for text in texts
|
|
477
|
+
])
|
|
478
|
+
|
|
479
|
+
# =========================================================================
|
|
480
|
+
# Async Methods - Score Complete
|
|
481
|
+
# =========================================================================
|
|
482
|
+
|
|
483
|
+
async def ascore_complete(
|
|
484
|
+
self,
|
|
485
|
+
text: str,
|
|
486
|
+
choices: ChoicesType,
|
|
487
|
+
system_prompt: str | None = None,
|
|
488
|
+
) -> ClassificationResult:
|
|
489
|
+
"""Async version of score_complete().
|
|
490
|
+
|
|
491
|
+
Score a classification using multi-call evaluation with softmax.
|
|
492
|
+
|
|
493
|
+
Args:
|
|
494
|
+
text: The text to classify.
|
|
495
|
+
choices: Either a list of choice labels, or a dict mapping labels to descriptions.
|
|
496
|
+
system_prompt: Optional custom system prompt.
|
|
497
|
+
|
|
498
|
+
Returns:
|
|
499
|
+
ClassificationResult with prediction, confidence, and probabilities.
|
|
500
|
+
"""
|
|
501
|
+
import asyncio
|
|
502
|
+
|
|
503
|
+
labels = get_choice_labels(choices)
|
|
504
|
+
system, user = build_classification_prompt(text, choices, system_prompt)
|
|
505
|
+
|
|
506
|
+
# Compute logprob for each choice concurrently
|
|
507
|
+
logprobs_tasks = [
|
|
508
|
+
self._aget_choice_logprob(system, user, label)
|
|
509
|
+
for label in labels
|
|
510
|
+
]
|
|
511
|
+
logprob_values = await asyncio.gather(*logprobs_tasks)
|
|
512
|
+
|
|
513
|
+
logprobs = dict(zip(labels, logprob_values))
|
|
514
|
+
|
|
515
|
+
# Apply softmax to get probabilities
|
|
516
|
+
probabilities = self._softmax(logprobs)
|
|
517
|
+
|
|
518
|
+
# Get prediction (highest probability)
|
|
519
|
+
prediction = max(probabilities, key=probabilities.get)
|
|
520
|
+
|
|
521
|
+
return ClassificationResult(
|
|
522
|
+
prediction=prediction,
|
|
523
|
+
confidence=probabilities[prediction],
|
|
524
|
+
probabilities=probabilities,
|
|
525
|
+
raw_response={"logprobs": logprobs},
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
async def abatch_score_complete(
|
|
529
|
+
self,
|
|
530
|
+
texts: List[str],
|
|
531
|
+
choices: ChoicesType,
|
|
532
|
+
system_prompt: str | None = None,
|
|
533
|
+
) -> List[ClassificationResult]:
|
|
534
|
+
"""Async version of batch_score_complete().
|
|
535
|
+
|
|
536
|
+
Score multiple texts using complete multi-call method.
|
|
537
|
+
|
|
538
|
+
Args:
|
|
539
|
+
texts: List of texts to classify.
|
|
540
|
+
choices: Either a list of choice labels, or a dict mapping labels to descriptions.
|
|
541
|
+
system_prompt: Optional custom system prompt.
|
|
542
|
+
|
|
543
|
+
Returns:
|
|
544
|
+
List of ClassificationResults, one per input text.
|
|
545
|
+
"""
|
|
546
|
+
import asyncio
|
|
547
|
+
return await asyncio.gather(*[
|
|
548
|
+
self.ascore_complete(text, choices, system_prompt) for text in texts
|
|
549
|
+
])
|
|
550
|
+
|
|
551
|
+
# =========================================================================
|
|
552
|
+
# Async Methods - Classify
|
|
553
|
+
# =========================================================================
|
|
554
|
+
|
|
555
|
+
async def aclassify(
|
|
556
|
+
self,
|
|
557
|
+
text: str,
|
|
558
|
+
choices: ChoicesType,
|
|
559
|
+
system_prompt: str | None = None,
|
|
560
|
+
) -> ClassificationResult:
|
|
561
|
+
"""Async version of classify().
|
|
562
|
+
|
|
563
|
+
Classify text with constrained generation and fast scoring.
|
|
564
|
+
|
|
565
|
+
Args:
|
|
566
|
+
text: The text to classify.
|
|
567
|
+
choices: Either a list of choice labels, or a dict mapping labels to descriptions.
|
|
568
|
+
system_prompt: Optional custom system prompt.
|
|
569
|
+
|
|
570
|
+
Returns:
|
|
571
|
+
ClassificationResult with prediction, confidence, and probabilities.
|
|
572
|
+
"""
|
|
573
|
+
return await self.ascore_fast(text, choices, system_prompt)
|
|
574
|
+
|
|
575
|
+
async def aclassify_complete(
|
|
576
|
+
self,
|
|
577
|
+
text: str,
|
|
578
|
+
choices: ChoicesType,
|
|
579
|
+
system_prompt: str | None = None,
|
|
580
|
+
) -> ClassificationResult:
|
|
581
|
+
"""Async version of classify_complete().
|
|
582
|
+
|
|
583
|
+
Classify text with constrained generation and complete scoring.
|
|
584
|
+
|
|
585
|
+
Args:
|
|
586
|
+
text: The text to classify.
|
|
587
|
+
choices: Either a list of choice labels, or a dict mapping labels to descriptions.
|
|
588
|
+
system_prompt: Optional custom system prompt.
|
|
589
|
+
|
|
590
|
+
Returns:
|
|
591
|
+
ClassificationResult with prediction, confidence, and probabilities.
|
|
592
|
+
"""
|
|
593
|
+
# Use generate for constrained prediction
|
|
594
|
+
prediction = await self.agenerate(text, choices, system_prompt)
|
|
595
|
+
|
|
596
|
+
# Use score_complete for probabilities
|
|
597
|
+
result = await self.ascore_complete(text, choices, system_prompt)
|
|
598
|
+
|
|
599
|
+
# Override prediction with constrained output
|
|
600
|
+
result.prediction = prediction
|
|
601
|
+
result.confidence = result.probabilities.get(prediction, 0.0)
|
|
602
|
+
|
|
603
|
+
return result
|
|
604
|
+
|
|
605
|
+
async def abatch_classify(
|
|
606
|
+
self,
|
|
607
|
+
texts: List[str],
|
|
608
|
+
choices: ChoicesType,
|
|
609
|
+
system_prompt: str | None = None,
|
|
610
|
+
) -> List[ClassificationResult]:
|
|
611
|
+
"""Async version of batch_classify().
|
|
612
|
+
|
|
613
|
+
Classify multiple texts with fast scoring.
|
|
614
|
+
|
|
615
|
+
Args:
|
|
616
|
+
texts: List of texts to classify.
|
|
617
|
+
choices: Either a list of choice labels, or a dict mapping labels to descriptions.
|
|
618
|
+
system_prompt: Optional custom system prompt.
|
|
619
|
+
|
|
620
|
+
Returns:
|
|
621
|
+
List of ClassificationResults, one per input text.
|
|
622
|
+
"""
|
|
623
|
+
import asyncio
|
|
624
|
+
return await asyncio.gather(*[
|
|
625
|
+
self.aclassify(text, choices, system_prompt) for text in texts
|
|
626
|
+
])
|
|
627
|
+
|
|
628
|
+
async def abatch_classify_complete(
|
|
629
|
+
self,
|
|
630
|
+
texts: List[str],
|
|
631
|
+
choices: ChoicesType,
|
|
632
|
+
system_prompt: str | None = None,
|
|
633
|
+
) -> List[ClassificationResult]:
|
|
634
|
+
"""Async version of batch_classify_complete().
|
|
635
|
+
|
|
636
|
+
Classify multiple texts with complete scoring.
|
|
637
|
+
|
|
638
|
+
Args:
|
|
639
|
+
texts: List of texts to classify.
|
|
640
|
+
choices: Either a list of choice labels, or a dict mapping labels to descriptions.
|
|
641
|
+
system_prompt: Optional custom system prompt.
|
|
642
|
+
|
|
643
|
+
Returns:
|
|
644
|
+
List of ClassificationResults, one per input text.
|
|
645
|
+
"""
|
|
646
|
+
import asyncio
|
|
647
|
+
return await asyncio.gather(*[
|
|
648
|
+
self.aclassify_complete(text, choices, system_prompt) for text in texts
|
|
649
|
+
])
|
|
650
|
+
|
|
651
|
+
# =========================================================================
|
|
652
|
+
# Private Helper Methods
|
|
653
|
+
# =========================================================================
|
|
654
|
+
|
|
655
|
+
def _extract_probabilities_from_logprobs(
|
|
656
|
+
self,
|
|
657
|
+
response: Dict[str, Any],
|
|
658
|
+
labels: List[str],
|
|
659
|
+
) -> Dict[str, float]:
|
|
660
|
+
"""Extract probabilities from logprobs in the response.
|
|
661
|
+
|
|
662
|
+
Looks for the decision token in the logprobs and extracts
|
|
663
|
+
the probability distribution for the valid choices.
|
|
664
|
+
|
|
665
|
+
Args:
|
|
666
|
+
response: The raw API response.
|
|
667
|
+
labels: List of valid choice labels.
|
|
668
|
+
|
|
669
|
+
Returns:
|
|
670
|
+
Dict mapping choice labels to probabilities.
|
|
671
|
+
"""
|
|
672
|
+
# Initialize with zero logprobs
|
|
673
|
+
choice_logprobs: Dict[str, float] = {label: float("-inf") for label in labels}
|
|
674
|
+
|
|
675
|
+
# Navigate the logprobs payload to find the decision token
|
|
676
|
+
logprobs_data = response.get("logprobs", [])
|
|
677
|
+
|
|
678
|
+
if not logprobs_data:
|
|
679
|
+
# No logprobs available, return uniform distribution
|
|
680
|
+
return {label: 1.0 / len(labels) for label in labels}
|
|
681
|
+
|
|
682
|
+
for token_data in logprobs_data:
|
|
683
|
+
# Check if this is a dict or list format
|
|
684
|
+
if isinstance(token_data, dict):
|
|
685
|
+
top_candidates = token_data.get("top_logprobs", [])
|
|
686
|
+
elif isinstance(token_data, (list, tuple)) and len(token_data) > 0:
|
|
687
|
+
# Might be in different format
|
|
688
|
+
top_candidates = token_data if isinstance(token_data[0], dict) else []
|
|
689
|
+
else:
|
|
690
|
+
continue
|
|
691
|
+
|
|
692
|
+
found_decision_token = False
|
|
693
|
+
|
|
694
|
+
for candidate in top_candidates:
|
|
695
|
+
if isinstance(candidate, dict):
|
|
696
|
+
token = candidate.get("token", "")
|
|
697
|
+
# Normalize the token
|
|
698
|
+
clean_token = token.strip(' \n\r\t"\'').lower()
|
|
699
|
+
|
|
700
|
+
# Check if this matches any of our labels
|
|
701
|
+
for label in labels:
|
|
702
|
+
if label.lower() == clean_token or clean_token in label.lower():
|
|
703
|
+
logprob = candidate.get("logprob", float("-inf"))
|
|
704
|
+
if logprob > choice_logprobs[label]:
|
|
705
|
+
choice_logprobs[label] = logprob
|
|
706
|
+
found_decision_token = True
|
|
707
|
+
|
|
708
|
+
if found_decision_token:
|
|
709
|
+
break
|
|
710
|
+
|
|
711
|
+
# Apply softmax to convert logprobs to probabilities
|
|
712
|
+
return self._softmax(choice_logprobs)
|
|
713
|
+
|
|
714
|
+
def _get_choice_logprob(
|
|
715
|
+
self,
|
|
716
|
+
system: str,
|
|
717
|
+
user: str,
|
|
718
|
+
choice: str,
|
|
719
|
+
) -> float:
|
|
720
|
+
"""Compute log P(choice | context) for a single choice.
|
|
721
|
+
|
|
722
|
+
Appends the choice as assistant response and measures
|
|
723
|
+
how "expected" those tokens were.
|
|
724
|
+
|
|
725
|
+
Args:
|
|
726
|
+
system: System prompt.
|
|
727
|
+
user: User prompt.
|
|
728
|
+
choice: The choice label to evaluate.
|
|
729
|
+
|
|
730
|
+
Returns:
|
|
731
|
+
Log probability of the choice given the context.
|
|
732
|
+
"""
|
|
733
|
+
messages = [
|
|
734
|
+
{"role": "system", "content": system},
|
|
735
|
+
{"role": "user", "content": user},
|
|
736
|
+
{"role": "assistant", "content": json.dumps({"label": choice})},
|
|
737
|
+
]
|
|
738
|
+
|
|
739
|
+
response = self._client.chat(
|
|
740
|
+
model=self._model,
|
|
741
|
+
messages=messages,
|
|
742
|
+
logprobs=True,
|
|
743
|
+
options={"num_predict": 0},
|
|
744
|
+
)
|
|
745
|
+
|
|
746
|
+
return self._extract_logprob_from_response(response)
|
|
747
|
+
|
|
748
|
+
async def _aget_choice_logprob(
|
|
749
|
+
self,
|
|
750
|
+
system: str,
|
|
751
|
+
user: str,
|
|
752
|
+
choice: str,
|
|
753
|
+
) -> float:
|
|
754
|
+
"""Async version of _get_choice_logprob().
|
|
755
|
+
|
|
756
|
+
Compute log P(choice | context) for a single choice.
|
|
757
|
+
|
|
758
|
+
Args:
|
|
759
|
+
system: System prompt.
|
|
760
|
+
user: User prompt.
|
|
761
|
+
choice: The choice label to evaluate.
|
|
762
|
+
|
|
763
|
+
Returns:
|
|
764
|
+
Log probability of the choice given the context.
|
|
765
|
+
"""
|
|
766
|
+
messages = [
|
|
767
|
+
{"role": "system", "content": system},
|
|
768
|
+
{"role": "user", "content": user},
|
|
769
|
+
{"role": "assistant", "content": json.dumps({"label": choice})},
|
|
770
|
+
]
|
|
771
|
+
|
|
772
|
+
response = await self._client.chat(
|
|
773
|
+
model=self._model,
|
|
774
|
+
messages=messages,
|
|
775
|
+
logprobs=True,
|
|
776
|
+
options={"num_predict": 0},
|
|
777
|
+
)
|
|
778
|
+
|
|
779
|
+
return self._extract_logprob_from_response(response)
|
|
780
|
+
|
|
781
|
+
def _extract_logprob_from_response(self, response: Dict[str, Any]) -> float:
|
|
782
|
+
"""Extract log probability from Ollama response.
|
|
783
|
+
|
|
784
|
+
Tries various fields where Ollama might return logprobs.
|
|
785
|
+
|
|
786
|
+
Args:
|
|
787
|
+
response: The raw API response.
|
|
788
|
+
|
|
789
|
+
Returns:
|
|
790
|
+
Extracted log probability, or 0.0 if not found.
|
|
791
|
+
"""
|
|
792
|
+
msg = response.get("message", {})
|
|
793
|
+
|
|
794
|
+
# Field: message.logprob (most common in newer versions)
|
|
795
|
+
if "logprob" in msg:
|
|
796
|
+
return msg["logprob"]
|
|
797
|
+
|
|
798
|
+
# Field: eval_prob
|
|
799
|
+
if "eval_prob" in response:
|
|
800
|
+
p = response["eval_prob"]
|
|
801
|
+
return math.log(p) if p > 0 else float("-inf")
|
|
802
|
+
|
|
803
|
+
# Field: logprobs list
|
|
804
|
+
if "logprobs" in msg:
|
|
805
|
+
lp = msg["logprobs"]
|
|
806
|
+
if isinstance(lp, list) and lp:
|
|
807
|
+
# Sum or average the logprobs
|
|
808
|
+
return sum(lp) / len(lp)
|
|
809
|
+
|
|
810
|
+
# Check in response logprobs
|
|
811
|
+
logprobs = response.get("logprobs", [])
|
|
812
|
+
if logprobs and isinstance(logprobs, list):
|
|
813
|
+
# Try to extract from first token's logprob
|
|
814
|
+
if isinstance(logprobs[0], dict) and "logprob" in logprobs[0]:
|
|
815
|
+
return logprobs[0]["logprob"]
|
|
816
|
+
|
|
817
|
+
return 0.0 # Default neutral
|
|
818
|
+
|
|
819
|
+
def _softmax(self, logprobs: Dict[str, float]) -> Dict[str, float]:
|
|
820
|
+
"""Apply numerically stable softmax to log probabilities.
|
|
821
|
+
|
|
822
|
+
Args:
|
|
823
|
+
logprobs: Dict mapping labels to log probabilities.
|
|
824
|
+
|
|
825
|
+
Returns:
|
|
826
|
+
Dict mapping labels to probabilities (summing to 1.0).
|
|
827
|
+
"""
|
|
828
|
+
# Filter out -inf values for computation
|
|
829
|
+
valid_logprobs = {k: v for k, v in logprobs.items() if v > float("-inf")}
|
|
830
|
+
|
|
831
|
+
if not valid_logprobs:
|
|
832
|
+
# If all are -inf, return uniform distribution
|
|
833
|
+
n = len(logprobs)
|
|
834
|
+
return {k: 1.0 / n for k in logprobs}
|
|
835
|
+
|
|
836
|
+
max_lp = max(valid_logprobs.values())
|
|
837
|
+
exp_vals = {k: math.exp(v - max_lp) if v > float("-inf") else 0.0
|
|
838
|
+
for k, v in logprobs.items()}
|
|
839
|
+
total = sum(exp_vals.values())
|
|
840
|
+
|
|
841
|
+
if total == 0:
|
|
842
|
+
n = len(logprobs)
|
|
843
|
+
return {k: 1.0 / n for k in logprobs}
|
|
844
|
+
|
|
845
|
+
return {k: v / total for k, v in exp_vals.items()}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Prompt building utilities for classification."""
|
|
2
|
+
|
|
3
|
+
from typing import List, Dict, Union
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def build_classification_prompt(
|
|
7
|
+
text: str,
|
|
8
|
+
choices: Union[List[str], Dict[str, str]],
|
|
9
|
+
system_prompt: str | None = None,
|
|
10
|
+
) -> tuple[str, str]:
|
|
11
|
+
"""Build the system and user prompts for classification.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
text: The text to classify.
|
|
15
|
+
choices: Either a list of choice labels, or a dict mapping labels to descriptions.
|
|
16
|
+
system_prompt: Optional custom system prompt.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
Tuple of (system_prompt, user_prompt) strings.
|
|
20
|
+
"""
|
|
21
|
+
# Build the choices section of the prompt
|
|
22
|
+
choices_text = _format_choices(choices)
|
|
23
|
+
|
|
24
|
+
# Default system prompt
|
|
25
|
+
if system_prompt is None:
|
|
26
|
+
system_prompt = (
|
|
27
|
+
"You are a precise text classifier. "
|
|
28
|
+
"Your task is to classify the given text into exactly one of the provided categories. "
|
|
29
|
+
"Respond with only the category label, nothing else."
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# Build user prompt
|
|
33
|
+
user_prompt = f"""Classify the following text into one of these categories:
|
|
34
|
+
|
|
35
|
+
{choices_text}
|
|
36
|
+
|
|
37
|
+
Text to classify:
|
|
38
|
+
{text}
|
|
39
|
+
|
|
40
|
+
Respond with only the category label."""
|
|
41
|
+
|
|
42
|
+
return system_prompt, user_prompt
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _format_choices(choices: Union[List[str], Dict[str, str]]) -> str:
|
|
46
|
+
"""Format choices for the prompt.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
choices: Either a list of choice labels, or a dict mapping labels to descriptions.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Formatted string representation of choices.
|
|
53
|
+
"""
|
|
54
|
+
if isinstance(choices, dict):
|
|
55
|
+
# Choices with descriptions
|
|
56
|
+
lines = []
|
|
57
|
+
for label, description in choices.items():
|
|
58
|
+
lines.append(f"- {label}: {description}")
|
|
59
|
+
return "\n".join(lines)
|
|
60
|
+
else:
|
|
61
|
+
# Simple list of choices
|
|
62
|
+
return "\n".join(f"- {choice}" for choice in choices)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def get_choice_labels(choices: Union[List[str], Dict[str, str]]) -> List[str]:
|
|
66
|
+
"""Extract the choice labels from either format.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
choices: Either a list of choice labels, or a dict mapping labels to descriptions.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
List of choice labels.
|
|
73
|
+
"""
|
|
74
|
+
if isinstance(choices, dict):
|
|
75
|
+
return list(choices.keys())
|
|
76
|
+
return list(choices)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def build_json_schema_for_choices(choices: List[str]) -> dict:
|
|
80
|
+
"""Build a JSON schema that constrains output to the given choices.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
choices: List of valid choice labels.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
JSON schema dict for structured output.
|
|
87
|
+
"""
|
|
88
|
+
return {
|
|
89
|
+
"type": "object",
|
|
90
|
+
"properties": {
|
|
91
|
+
"label": {
|
|
92
|
+
"type": "string",
|
|
93
|
+
"enum": choices,
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
"required": ["label"],
|
|
97
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Type definitions for ollama-classifier."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Dict, Any, Union, List
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class ClassificationResult:
|
|
9
|
+
"""Result of a classification operation.
|
|
10
|
+
|
|
11
|
+
Attributes:
|
|
12
|
+
prediction: The predicted class label.
|
|
13
|
+
confidence: Confidence score for the prediction (0.0 to 1.0).
|
|
14
|
+
probabilities: Probability distribution over all choices.
|
|
15
|
+
raw_response: Raw response from Ollama API for debugging.
|
|
16
|
+
"""
|
|
17
|
+
prediction: str
|
|
18
|
+
confidence: float
|
|
19
|
+
probabilities: Dict[str, float]
|
|
20
|
+
raw_response: Dict[str, Any] = field(default_factory=dict)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# Type alias for choices - can be a list of labels or dict mapping labels to descriptions
|
|
24
|
+
ChoicesType = Union[List[str], Dict[str, str]]
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: ollama-classifier
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A wrapper around Ollama Python SDK for text classification with constrained output and confidence scoring
|
|
5
|
+
Author: Luigi Palumbo, Mengting Yu
|
|
6
|
+
Author-email: Luigi Palumbo <paluigi@gmail.com>, Mengting Yu <mengting.yu@unitus.it>
|
|
7
|
+
Requires-Dist: ollama>=0.4.0
|
|
8
|
+
Requires-Dist: sphinx>=7.0.0 ; extra == 'docs'
|
|
9
|
+
Requires-Dist: sphinx-rtd-theme>=2.0.0 ; extra == 'docs'
|
|
10
|
+
Requires-Dist: myst-parser>=2.0.0 ; extra == 'docs'
|
|
11
|
+
Requires-Python: >=3.11
|
|
12
|
+
Provides-Extra: docs
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# ollama-classifier
|
|
16
|
+
|
|
17
|
+
A Python wrapper around the Ollama Python SDK for text classification with constrained output and confidence scoring.
|
|
18
|
+
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
- **Constrained Output**: Uses JSON schema with enum constraints to ensure only valid choices are generated
|
|
22
|
+
- **Confidence Scoring**: Two methods available:
|
|
23
|
+
- **Fast**: Single API call with logprob extraction
|
|
24
|
+
- **Complete**: Multi-call evaluation with softmax for calibrated probabilities
|
|
25
|
+
- **Sync & Async**: Full support for both synchronous and asynchronous operations
|
|
26
|
+
- **Batch Processing**: Classify multiple texts efficiently
|
|
27
|
+
- **Flexible Choices**: Support for simple labels or labels with descriptions
|
|
28
|
+
- **Custom Prompts**: Override the default system prompt for specialized tasks
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install ollama-classifier
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Or with uv:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
uv add ollama-classifier
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Prerequisites
|
|
43
|
+
|
|
44
|
+
- [Ollama](https://ollama.com/download) installed and running
|
|
45
|
+
- A model pulled (e.g., `ollama pull llama3.2`)
|
|
46
|
+
|
|
47
|
+
## Quick Start
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from ollama import Client
|
|
51
|
+
from ollama_classifier import OllamaClassifier
|
|
52
|
+
|
|
53
|
+
client = Client()
|
|
54
|
+
classifier = OllamaClassifier(client, model="llama3.2")
|
|
55
|
+
|
|
56
|
+
result = classifier.classify(
|
|
57
|
+
text="I love this product!",
|
|
58
|
+
choices=["positive", "negative", "neutral"]
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
print(f"Prediction: {result.prediction}")
|
|
62
|
+
print(f"Confidence: {result.confidence:.2%}")
|
|
63
|
+
print(f"Probabilities: {result.probabilities}")
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Usage
|
|
67
|
+
|
|
68
|
+
### Basic Classification
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from ollama import Client
|
|
72
|
+
from ollama_classifier import OllamaClassifier
|
|
73
|
+
|
|
74
|
+
client = Client()
|
|
75
|
+
classifier = OllamaClassifier(client, model="llama3.2")
|
|
76
|
+
|
|
77
|
+
result = classifier.classify(
|
|
78
|
+
text="The goalkeeper made an incredible save!",
|
|
79
|
+
choices=["sports", "politics", "technology", "entertainment"]
|
|
80
|
+
)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Classification with Label Descriptions
|
|
84
|
+
|
|
85
|
+
Providing descriptions helps the model understand each category better:
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
choices = {
|
|
89
|
+
"positive": "Text expresses happiness, satisfaction, or approval",
|
|
90
|
+
"negative": "Text expresses anger, disappointment, or disapproval",
|
|
91
|
+
"mixed": "Text contains both positive and negative sentiments",
|
|
92
|
+
"neutral": "Text is factual without strong emotional content",
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
result = classifier.classify(
|
|
96
|
+
text="The food was amazing but the service was terrible.",
|
|
97
|
+
choices=choices
|
|
98
|
+
)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Custom System Prompt
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
result = classifier.classify(
|
|
105
|
+
text="The quarterly earnings exceeded analyst expectations.",
|
|
106
|
+
choices=["bullish", "bearish", "neutral"],
|
|
107
|
+
system_prompt="You are a financial sentiment analyzer. "
|
|
108
|
+
"Classify financial news based on market sentiment."
|
|
109
|
+
)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Scoring Methods
|
|
113
|
+
|
|
114
|
+
#### Fast Scoring (Single API Call)
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
result = classifier.score_fast(
|
|
118
|
+
text="The movie was fantastic!",
|
|
119
|
+
choices=["positive", "negative", "neutral"]
|
|
120
|
+
)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
#### Complete Scoring (Multi-Call with Softmax)
|
|
124
|
+
|
|
125
|
+
More accurate but makes N API calls for N choices:
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
result = classifier.score_complete(
|
|
129
|
+
text="The movie was fantastic!",
|
|
130
|
+
choices=["positive", "negative", "neutral"]
|
|
131
|
+
)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Generate Only (Fastest)
|
|
135
|
+
|
|
136
|
+
When you only need the prediction without confidence scores:
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
prediction = classifier.generate(
|
|
140
|
+
text="The team won the championship!",
|
|
141
|
+
choices=["sports", "finance", "politics"]
|
|
142
|
+
)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Batch Classification
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
texts = [
|
|
149
|
+
"The goalkeeper made an incredible save!",
|
|
150
|
+
"The central bank raised interest rates.",
|
|
151
|
+
"The new smartphone features a revolutionary camera.",
|
|
152
|
+
]
|
|
153
|
+
|
|
154
|
+
# Fast batch classification
|
|
155
|
+
results = classifier.batch_classify(
|
|
156
|
+
texts=texts,
|
|
157
|
+
choices=["sports", "finance", "technology"]
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# Complete batch classification (more accurate)
|
|
161
|
+
results = classifier.batch_classify_complete(
|
|
162
|
+
texts=texts,
|
|
163
|
+
choices=["sports", "finance", "technology"]
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
for text, result in zip(texts, results):
|
|
167
|
+
print(f"{text} -> {result.prediction} ({result.confidence:.2%})")
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Async Usage
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
import asyncio
|
|
174
|
+
from ollama import AsyncClient
|
|
175
|
+
from ollama_classifier import OllamaClassifier
|
|
176
|
+
|
|
177
|
+
async def main():
|
|
178
|
+
client = AsyncClient()
|
|
179
|
+
classifier = OllamaClassifier(client, model="llama3.2")
|
|
180
|
+
|
|
181
|
+
# Single classification
|
|
182
|
+
result = await classifier.aclassify(
|
|
183
|
+
text="The concert was amazing!",
|
|
184
|
+
choices=["positive", "negative", "neutral"]
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
# Batch classification (concurrent)
|
|
188
|
+
results = await classifier.abatch_classify(
|
|
189
|
+
texts=["Text 1", "Text 2", "Text 3"],
|
|
190
|
+
choices=["positive", "negative", "neutral"]
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
asyncio.run(main())
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## API Reference
|
|
197
|
+
|
|
198
|
+
### ClassificationResult
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
@dataclass
|
|
202
|
+
class ClassificationResult:
|
|
203
|
+
prediction: str # The predicted choice label
|
|
204
|
+
confidence: float # Confidence score (0.0 to 1.0)
|
|
205
|
+
probabilities: Dict[str, float] # Probability distribution over all choices
|
|
206
|
+
raw_response: Dict # Raw Ollama response for debugging
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### OllamaClassifier Methods
|
|
210
|
+
|
|
211
|
+
| Method | Async | Description |
|
|
212
|
+
|--------|-------|-------------|
|
|
213
|
+
| `generate(text, choices, system_prompt)` | `agenerate` | Constrained output only (fastest) |
|
|
214
|
+
| `score_fast(text, choices, system_prompt)` | `ascore_fast` | Single-call logprob extraction |
|
|
215
|
+
| `score_complete(text, choices, system_prompt)` | `ascore_complete` | Multi-call evaluation with softmax |
|
|
216
|
+
| `classify(text, choices, system_prompt)` | `aclassify` | Generate + score_fast |
|
|
217
|
+
| `classify_complete(text, choices, system_prompt)` | `aclassify_complete` | Generate + score_complete |
|
|
218
|
+
| `batch_generate(texts, choices, system_prompt)` | `abatch_generate` | Batch constrained output |
|
|
219
|
+
| `batch_score_fast(texts, choices, system_prompt)` | `abatch_score_fast` | Batch fast scoring |
|
|
220
|
+
| `batch_score_complete(texts, choices, system_prompt)` | `abatch_score_complete` | Batch complete scoring |
|
|
221
|
+
| `batch_classify(texts, choices, system_prompt)` | `abatch_classify` | Batch classify (fast) |
|
|
222
|
+
| `batch_classify_complete(texts, choices, system_prompt)` | `abatch_classify_complete` | Batch classify (complete) |
|
|
223
|
+
|
|
224
|
+
### Parameters
|
|
225
|
+
|
|
226
|
+
- **text** (str): The text to classify
|
|
227
|
+
- **texts** (List[str]): List of texts to classify (batch methods)
|
|
228
|
+
- **choices** (Union[List[str], Dict[str, str]]): Either a list of choice labels, or a dict mapping labels to descriptions
|
|
229
|
+
- **system_prompt** (str | None): Optional custom system prompt
|
|
230
|
+
|
|
231
|
+
## Choosing a Method
|
|
232
|
+
|
|
233
|
+
| Use Case | Recommended Method |
|
|
234
|
+
|----------|-------------------|
|
|
235
|
+
| Speed is critical, no confidence needed | `generate` |
|
|
236
|
+
| Speed with confidence scores | `classify` / `score_fast` |
|
|
237
|
+
| Accurate confidence scores | `classify_complete` / `score_complete` |
|
|
238
|
+
| Batch processing | `batch_classify` or `batch_classify_complete` |
|
|
239
|
+
| Concurrent processing | Async variants (`aclassify`, etc.) |
|
|
240
|
+
|
|
241
|
+
## License
|
|
242
|
+
|
|
243
|
+
MIT License
|
|
244
|
+
|
|
245
|
+
## Development
|
|
246
|
+
|
|
247
|
+
This project just started! Looking forward to suggestions, issues, and pull requests!
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
ollama_classifier/__init__.py,sha256=r5LFD-YlcVQKMJ1RUTtaZV6WlyHCFdsWwFN-12tKhLE,862
|
|
2
|
+
ollama_classifier/classifier.py,sha256=4XwDRqJ8EIbnNlL8Zp0TP0Ff6QMRJkYczxhiDe2jL40,30140
|
|
3
|
+
ollama_classifier/prompts.py,sha256=54YuHN7941LXtY-S7-krfPurzzvULunCbA84PMyVhk0,2746
|
|
4
|
+
ollama_classifier/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
ollama_classifier/types.py,sha256=UU9R8TksYfjKKJ1ZRoyr5FTsFlCv2rLx2Kt6K2-P1o0,773
|
|
6
|
+
ollama_classifier-0.1.0.dist-info/WHEEL,sha256=mydTeHxOpFHo-DnYhAd_3ATePms-g4rrYvM7wJK8P-U,80
|
|
7
|
+
ollama_classifier-0.1.0.dist-info/METADATA,sha256=JEHvA1nLU5JYE0g1v35GfqnTt6_dqjKjQiQHCRq_YYw,7414
|
|
8
|
+
ollama_classifier-0.1.0.dist-info/RECORD,,
|