mirror of
https://github.com/ollama/ollama-python.git
synced 2026-11-12 02:30:39 +00:00
feat: add System One API support (#749)
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
@@ -14,6 +14,18 @@ from ollama._types import (
|
||||
ResponseError,
|
||||
ShowResponse,
|
||||
StatusResponse,
|
||||
SystemOneAnswer,
|
||||
SystemOneChoiceAnswer,
|
||||
SystemOneChoiceQuestion,
|
||||
SystemOneContent,
|
||||
SystemOneNoulAnswer,
|
||||
SystemOneNoulQuestion,
|
||||
SystemOneQuestion,
|
||||
SystemOneRequest,
|
||||
SystemOneResponse,
|
||||
SystemOneScoreAnswer,
|
||||
SystemOneScoreQuestion,
|
||||
SystemOneUsage,
|
||||
Tool,
|
||||
WebFetchResponse,
|
||||
WebSearchResponse,
|
||||
@@ -36,6 +48,18 @@ __all__ = [
|
||||
'ResponseError',
|
||||
'ShowResponse',
|
||||
'StatusResponse',
|
||||
'SystemOneAnswer',
|
||||
'SystemOneChoiceAnswer',
|
||||
'SystemOneChoiceQuestion',
|
||||
'SystemOneContent',
|
||||
'SystemOneNoulAnswer',
|
||||
'SystemOneNoulQuestion',
|
||||
'SystemOneQuestion',
|
||||
'SystemOneRequest',
|
||||
'SystemOneResponse',
|
||||
'SystemOneScoreAnswer',
|
||||
'SystemOneScoreQuestion',
|
||||
'SystemOneUsage',
|
||||
'Tool',
|
||||
'WebFetchResponse',
|
||||
'WebSearchResponse',
|
||||
@@ -57,3 +81,4 @@ show = _client.show
|
||||
ps = _client.ps
|
||||
web_search = _client.web_search
|
||||
web_fetch = _client.web_fetch
|
||||
systemone = _client.systemone
|
||||
|
||||
@@ -66,6 +66,10 @@ from ollama._types import (
|
||||
ShowRequest,
|
||||
ShowResponse,
|
||||
StatusResponse,
|
||||
SystemOneContent,
|
||||
SystemOneQuestion,
|
||||
SystemOneRequest,
|
||||
SystemOneResponse,
|
||||
Tool,
|
||||
WebFetchRequest,
|
||||
WebFetchResponse,
|
||||
@@ -403,6 +407,29 @@ class Client(BaseClient):
|
||||
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 = '',
|
||||
@@ -1036,6 +1063,29 @@ class AsyncClient(BaseClient):
|
||||
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 = '',
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user