Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,47 @@ ollama.embed(model='gemma4', input=['The sky is blue because of rayleigh scatter
ollama.ps()
```

### System One

```python
import ollama

response = ollama.systemone(
model='nimble',
state='Our checkout has returned 500 errors since 9am.',
questions={
'team': {
'type': 'choice',
'instructions': 'Which team should handle this ticket?',
'criteria': {'billing': 'Payments and refunds', 'technical': 'Software errors'},
},
},
)
print(response.answers['team'])
```

System One uses `POST /v1/systemone` and requires Ollama v0.35.0 or later with a
compatible local model such as `nimble`. It returns one JSON response; streaming
and cloud models are not supported.

`state` and question `instructions` accept text, JSON objects, or arrays. Questions
are evaluated in their supplied order:

- `choice`: 2–26 option keys mapped to descriptions; `None` uses the key as its description.
- `noul`: probability of true, with optional `{"false": "No", "true": "Yes"}` descriptions.
- `score`: 2–26 descriptions ordered lowest to highest; returns a potentially fractional, zero-based score.

Responses contain `model`, `answers`, and `usage.input_tokens` / `usage.output_tokens`.
Confidence measures probability concentration, not calibrated correctness.
Token usage comes from the server; output tokens are not necessarily zero.
Optional `keep_alive` accepts seconds or a duration string. Requests use the client's
existing host, headers, and HTTP error handling. The server validates its body and
model context limits without truncating input.

Available as `ollama.systemone`, `Client.systemone`, and `await AsyncClient.systemone`.
Question and answer types, including `SystemOneResponse`, are exported from `ollama`.
See [the combined question example](examples/systemone.py).

## Errors

Errors are raised if requests return an error status or if an error is detected while streaming.
Expand Down
4 changes: 4 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,7 @@ Requirement: `pip install tqdm`
### Thinking (levels) - Choose the thinking level

- [thinking-levels.py](thinking-levels.py)

### System One - Score typed questions

- [systemone.py](systemone.py) - Choice, boolean probability, and rubric score questions
13 changes: 13 additions & 0 deletions examples/systemone.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import ollama

# Requires Ollama v0.35.0 or later and the local nimble model.
response = ollama.systemone(
model='nimble',
state={'ticket': 'I was charged twice. Please refund the extra payment.'},
questions={
'team': {'type': 'choice', 'instructions': 'Which team should handle this ticket?', 'criteria': {'billing': 'Payments and refunds', 'technical': 'Bugs and integrations', 'other': 'None of the above'}},
'refund': {'type': 'noul', 'instructions': 'Does the customer explicitly ask for a refund?'},
'urgency': {'type': 'score', 'instructions': 'How urgent is this ticket?', 'criteria': ['Routine', 'Soon', 'Urgent']},
},
)
print(response.model_dump())
25 changes: 25 additions & 0 deletions ollama/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,18 @@
ResponseError,
ShowResponse,
StatusResponse,
SystemOneAnswer,
SystemOneChoiceAnswer,
SystemOneChoiceQuestion,
SystemOneContent,
SystemOneNoulAnswer,
SystemOneNoulQuestion,
SystemOneQuestion,
SystemOneRequest,
SystemOneResponse,
SystemOneScoreAnswer,
SystemOneScoreQuestion,
SystemOneUsage,
Tool,
WebFetchResponse,
WebSearchResponse,
Expand All @@ -36,6 +48,18 @@
'ResponseError',
'ShowResponse',
'StatusResponse',
'SystemOneAnswer',
'SystemOneChoiceAnswer',
'SystemOneChoiceQuestion',
'SystemOneContent',
'SystemOneNoulAnswer',
'SystemOneNoulQuestion',
'SystemOneQuestion',
'SystemOneRequest',
'SystemOneResponse',
'SystemOneScoreAnswer',
'SystemOneScoreQuestion',
'SystemOneUsage',
'Tool',
'WebFetchResponse',
'WebSearchResponse',
Expand All @@ -57,3 +81,4 @@
ps = _client.ps
web_search = _client.web_search
web_fetch = _client.web_fetch
systemone = _client.systemone
50 changes: 50 additions & 0 deletions ollama/_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@
ShowRequest,
ShowResponse,
StatusResponse,
SystemOneContent,
SystemOneQuestion,
SystemOneRequest,
SystemOneResponse,
Tool,
WebFetchRequest,
WebFetchResponse,
Expand Down Expand Up @@ -403,6 +407,29 @@ def add_two_numbers(a: int, b: int) -> int:
stream=stream,
)

def systemone(
self,
model: str,
state: SystemOneContent,
questions: Mapping[str, Union[SystemOneQuestion, Mapping[str, Any]]],
keep_alive: Optional[Union[float, str]] = None,
) -> SystemOneResponse:
"""Score choice, noul, and score questions with a local System One model.

Returns one JSON response. Streaming is not supported.
"""
return self._request(
SystemOneResponse,
'POST',
'/v1/systemone',
json=SystemOneRequest(
model=model,
state=state,
questions=questions,
keep_alive=keep_alive,
).model_dump(exclude_none=True),
)

def embed(
self,
model: str = '',
Expand Down Expand Up @@ -1036,6 +1063,29 @@ def add_two_numbers(a: int, b: int) -> int:
stream=stream,
)

async def systemone(
self,
model: str,
state: SystemOneContent,
questions: Mapping[str, Union[SystemOneQuestion, Mapping[str, Any]]],
keep_alive: Optional[Union[float, str]] = None,
) -> SystemOneResponse:
"""Score choice, noul, and score questions with a local System One model.

Returns one JSON response. Streaming is not supported.
"""
return await self._request(
SystemOneResponse,
'POST',
'/v1/systemone',
json=SystemOneRequest(
model=model,
state=state,
questions=questions,
keep_alive=keep_alive,
).model_dump(exclude_none=True),
)

async def embed(
self,
model: str = '',
Expand Down
70 changes: 70 additions & 0 deletions ollama/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,76 @@ class WebFetchResponse(SubscriptableBaseModel):
links: Optional[Sequence[str]] = None


SystemOneContent = Union[str, Dict[str, Any], List[Any]]
"""Text or JSON object/array rendered as text by System One."""


class SystemOneChoiceQuestion(SubscriptableBaseModel):
type: Literal['choice'] = 'choice'
instructions: SystemOneContent
criteria: Dict[str, Optional[str]]
'Ordered choices; a null description uses the choice key.'


class SystemOneNoulQuestion(SubscriptableBaseModel):
type: Literal['noul'] = 'noul'
instructions: SystemOneContent
criteria: Optional[Dict[Literal['false', 'true'], str]] = None


class SystemOneScoreQuestion(SubscriptableBaseModel):
type: Literal['score'] = 'score'
instructions: SystemOneContent
criteria: List[str]
'Descriptions ordered from lowest to highest score.'


SystemOneQuestion = Annotated[Union[SystemOneChoiceQuestion, SystemOneNoulQuestion, SystemOneScoreQuestion], Field(discriminator='type')]


class SystemOneRequest(SubscriptableBaseModel):
model: str
state: SystemOneContent
questions: Dict[str, SystemOneQuestion]
keep_alive: Optional[Union[float, str]] = None


class SystemOneNoulAnswer(SubscriptableBaseModel):
type: Literal['noul']
noul: float
'Probability of true.'


class SystemOneChoiceAnswer(SubscriptableBaseModel):
type: Literal['choice']
choice: str
probabilities: Dict[str, float]
confidence: float


class SystemOneScoreAnswer(SubscriptableBaseModel):
type: Literal['score']
score: float
'Expected zero-based score, which may be fractional.'
legend: Dict[str, str]
probabilities: Dict[str, float]
confidence: float


SystemOneAnswer = Annotated[Union[SystemOneNoulAnswer, SystemOneChoiceAnswer, SystemOneScoreAnswer], Field(discriminator='type')]


class SystemOneUsage(SubscriptableBaseModel):
input_tokens: int
output_tokens: int


class SystemOneResponse(SubscriptableBaseModel):
model: str
answers: Dict[str, SystemOneAnswer]
usage: SystemOneUsage


class RequestError(Exception):
"""
Common class for request errors.
Expand Down
88 changes: 88 additions & 0 deletions tests/test_systemone.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import inspect
import json

import httpx
import pytest

import ollama


@pytest.fixture
def anyio_backend():
return 'asyncio'


@pytest.mark.anyio
@pytest.mark.parametrize('variant', ['sync', 'async', 'module'])
async def test_systemone_wire_and_answers(variant, monkeypatch):
questions = {
'team': {'type': 'choice', 'instructions': {'task': 'Route ticket'}, 'criteria': {'technical': None, 'billing': 'Payments'}},
'refund': ollama.SystemOneNoulQuestion(instructions=['Refund requested?']),
'urgency': {'type': 'score', 'instructions': 'Urgency?', 'criteria': ['Routine', 'Urgent']},
}
expected = {
'model': 'nimble',
'state': {'ticket': ['Duplicate charge', None], 'paid': True},
'questions': {
'team': {'type': 'choice', 'instructions': {'task': 'Route ticket'}, 'criteria': {'technical': None, 'billing': 'Payments'}},
'refund': {'type': 'noul', 'instructions': ['Refund requested?']},
'urgency': {'type': 'score', 'instructions': 'Urgency?', 'criteria': ['Routine', 'Urgent']},
},
}
# Exercise both omission and numeric zero without duplicating the client matrix.
if variant == 'sync':
expected['keep_alive'] = 0
elif variant == 'async':
expected['keep_alive'] = '5m'
result = {
'model': 'nimble',
'answers': {
'team': {'type': 'choice', 'choice': 'billing', 'probabilities': {'technical': 0.2, 'billing': 0.8}, 'confidence': 0.3},
'refund': {'type': 'noul', 'noul': 0.9},
'urgency': {'type': 'score', 'score': 0.25, 'legend': {'0': 'Routine', '1': 'Urgent'}, 'probabilities': {'0': 0.75, '1': 0.25}, 'confidence': 0.2},
},
'usage': {'input_tokens': 123, 'output_tokens': 4},
}

def handle(request):
assert request.method == 'POST'
assert str(request.url) == 'http://sdk.test:11474/v1/systemone'
assert request.headers['authorization'] == 'Bearer synthetic'
body = json.loads(request.content)
assert body == expected
assert list(body['questions']) == ['team', 'refund', 'urgency']
assert list(body['questions']['team']['criteria']) == ['technical', 'billing']
return httpx.Response(200, json=result)

transport = httpx.MockTransport(handle)
if variant == 'module':
monkeypatch.setattr(ollama._client, '_client', httpx.Client(base_url='http://sdk.test:11474', headers={'authorization': 'Bearer synthetic'}, transport=transport))
method = ollama.systemone
else:
client = (ollama.AsyncClient if variant == 'async' else ollama.Client)(host='http://sdk.test:11474', headers={'authorization': 'Bearer synthetic'}, transport=transport)
method = client.systemone
kwargs = {key: expected[key] for key in ('model', 'state', 'keep_alive') if key in expected}
response = method(**kwargs, questions=questions)
if inspect.isawaitable(response):
response = await response
assert response.model_dump() == result
assert isinstance(response.answers['team'], ollama.SystemOneChoiceAnswer)
assert isinstance(response.answers['refund'], ollama.SystemOneNoulAnswer)
assert isinstance(response.answers['urgency'], ollama.SystemOneScoreAnswer)
assert response['answers']['urgency']['score'] == 0.25


@pytest.mark.anyio
@pytest.mark.parametrize('client_type', [ollama.Client, ollama.AsyncClient])
@pytest.mark.parametrize('status,message', [(400, 'state must not be empty'), (404, "model 'missing' not found"), (413, 'request body must not exceed 64 KiB'), (503, 'server busy')])
async def test_systemone_http_errors(client_type, status, message):
def handle(request):
return httpx.Response(status, json={'error': message})

client = client_type(transport=httpx.MockTransport(handle))
with pytest.raises(ollama.ResponseError) as error:
response = client.systemone('nimble', 'ticket', {'refund': {'type': 'noul', 'instructions': 'Refund?'}})
if inspect.isawaitable(response):
await response
assert error.value.status_code == status
assert error.value.error == message
Loading