Author an editorial HTML+SVG diagram with the Diagram Design kit - choose the visual type, read its reference, start from an example, and satisfy the safety and accessibility contract that diagram_write({kind:"html"}) enforces.
中文简介正在准备中。当前显示原文说明。
The `ollama` Python package used properly - chat and generate with streaming, embeddings, tool calling, structured outputs with a JSON Schema, the async client, keep_alive, timeouts and cancellation - and a project shape whose benchmark arithmetic is unit-tested against a fake transport while the live half skips loudly when no daemon answers.
中文简介正在准备中。当前显示原文说明。
查看安装方法安装前可以查看交给 AI 代理的指令内容。
pip install ollama is the official client: a thin wrapper over the REST API in
this pack's ollama-api skill, with pydantic response models. It ships a sync
Client, an AsyncClient, and module-level helpers that use a default client.
Every example below is self-contained: it reads OLLAMA_HOST itself, so it can
be copied out of this page on its own.
The daemon is on 127.0.0.1:11434 by default, but a person may have moved it.
Read the environment, do not bake the address into the code:
import os
from ollama import Client
HOST = os.environ.get("OLLAMA_HOST", "http://127.0.0.1:11434")
client = Client(host=HOST)
print(client.list().models)
OLLAMA_HOST may arrive as host:port without a scheme, because the daemon
accepts that spelling for its BIND address. Normalise it before a client uses
it, or an error message will read host:port and look like a hostname:
import os
def daemon_url() -> str:
raw = os.environ.get("OLLAMA_HOST", "127.0.0.1:11434").strip()
if "://" not in raw:
raw = "http://" + raw
return raw.rstrip("/")
import os
from ollama import Client
client = Client(host=os.environ.get("OLLAMA_HOST", "http://127.0.0.1:11434"))
# generate: one string in, text and timings out
answer = client.generate(model="qwen2.5:7b", prompt="Why is the sky blue?", options={"num_predict": 128})
print(answer.response)
print(answer.eval_count, answer.eval_duration) # tokens, NANOSECONDS
# chat: messages in, the same timings out
chat = client.chat(
model="qwen2.5:7b",
messages=[
{"role": "system", "content": "Answer in one sentence."},
{"role": "user", "content": "Why is the sky blue?"},
],
options={"temperature": 0, "seed": 42},
)
print(chat.message.content)
Every response model carries the same metric fields as the API:
eval_count, eval_duration, prompt_eval_count, prompt_eval_duration,
load_duration, total_duration and done_reason. They are the daemon's
numbers, in nanoseconds; the arithmetic in the ollama-inference-metrics skill
applies unchanged.
With stream=True the call returns a generator of partial responses. Each part
has .message.content (chat) or .response (generate); only the FINAL part
carries the counts and durations:
import os
from ollama import Client
client = Client(host=os.environ.get("OLLAMA_HOST", "http://127.0.0.1:11434"))
stream = client.chat(
model="qwen2.5:7b",
messages=[{"role": "user", "content": "Write one paragraph about the sea."}],
stream=True,
)
text = ""
final = None
try:
for part in stream:
text += part.message.content
if part.done:
final = part
finally:
stream.close()
print(text)
if final is None:
raise RuntimeError("the stream ended without a done part")
print(final.eval_count / (final.eval_duration / 1e9), "tok/s")
Two habits worth keeping:
for over stream leaves the HTTP
response open until garbage collection; stream.close() in a finally is
what tells the daemon to stop generating into a socket nobody reads;import os
from ollama import Client
client = Client(host=os.environ.get("OLLAMA_HOST", "http://127.0.0.1:11434"))
response = client.embed(model="nomic-embed-text", input=["the cat sat", "a cat was sitting"])
first, second = response.embeddings
print(len(first), "dimensions")
dot = sum(a * b for a, b in zip(first, second))
norm = (sum(a * a for a in first) ** 0.5) * (sum(b * b for b in second) ** 0.5)
print("cosine", dot / norm)
client.embed uses /api/embed (many inputs, one request). The older
client.embeddings(model=..., prompt=...) is the single-prompt form. Check the
model's capability first - a completion model refuses, and the client raises:
import os
from ollama import Client, ResponseError
client = Client(host=os.environ.get("OLLAMA_HOST", "http://127.0.0.1:11434"))
try:
client.embed(model="qwen2.5:7b", input="hello")
except ResponseError as error:
print(error.status_code, error.error)
# -> the model is not an embedding model; pull nomic-embed-text
Pass tools as JSON-Schema function definitions. The model answers with
message.tool_calls and no content; run the function and append a tool
message. This is a loop, and it must be bounded:
import os
from ollama import Client
client = Client(host=os.environ.get("OLLAMA_HOST", "http://127.0.0.1:11434"))
def get_weather(city: str) -> str:
return f"18 C and clear in {city}"
TOOLS = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Current weather for a city",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
},
}
]
messages = [{"role": "user", "content": "What is the weather in Lisbon?"}]
for _ in range(4): # bound the loop: a model can call a tool forever
response = client.chat(model="qwen2.5:7b", messages=messages, tools=TOOLS)
messages.append(response.message)
if not response.message.tool_calls:
print(response.message.content)
break
for call in response.message.tool_calls:
result = get_weather(**call.function.arguments)
messages.append({"role": "tool", "content": result, "tool_name": call.function.name})
else:
raise RuntimeError("the model kept calling tools; stop and inspect the conversation")
format="json" asks for JSON; a JSON Schema asks for a shape. With pydantic the
schema comes from the model itself:
import os
from ollama import Client
from pydantic import BaseModel
class Person(BaseModel):
name: str
age: int
client = Client(host=os.environ.get("OLLAMA_HOST", "http://127.0.0.1:11434"))
response = client.chat(
model="qwen2.5:7b",
messages=[{"role": "user", "content": "Give me one fictional person."}],
format=Person.model_json_schema(),
options={"temperature": 0},
)
person = Person.model_validate_json(response.message.content)
print(person.name, person.age)
The schema is a strong request, not a guarantee: model_validate_json is what
turns "the model probably obeyed" into an exception you can handle.
import asyncio
import os
from ollama import AsyncClient
HOST = os.environ.get("OLLAMA_HOST", "http://127.0.0.1:11434")
async def ask(question: str) -> str:
async with AsyncClient(host=HOST, timeout=120.0) as client:
response = await client.chat(
model="qwen2.5:7b",
messages=[{"role": "user", "content": question}],
options={"num_predict": 256},
)
return response.message.content
async def main() -> None:
try:
print(await asyncio.wait_for(ask("hi"), timeout=60))
except asyncio.TimeoutError:
print("cancelled; the daemon drops the request when the socket closes")
asyncio.run(main())
Three rules:
num_predict can keep a GPU busy for a moment after you left;timeout= on the client bounds connect AND read; pass a value larger than
the slowest cold load you accept, or set it per call;AsyncClient across an event-loop boundary. Create it in
the loop that uses it (async with is the simplest way to get that right).keep_alive and residencykeep_alive is a keyword argument on every generation call: a duration string,
0 to unload when the call ends, or -1 to keep the weights forever.
import os
from ollama import Client
client = Client(host=os.environ.get("OLLAMA_HOST", "http://127.0.0.1:11434"))
client.generate(model="qwen2.5:7b", prompt="warm up", keep_alive="10m", options={"num_predict": 1})
# ... many calls reuse the resident weights ...
client.generate(model="qwen2.5:7b", keep_alive=0, options={"num_predict": 0})
The last call is the unload (num_predict: 0 keeps it from generating, so the
warm-up and the unload each cost one prompt pass rather than an answer). Without
it, a "cold" measurement is not cold.
A benchmark needs arithmetic you can test without a model, and a live half that skips when no daemon answers. That is four small files:
pyproject.toml
src/ollama_lab/__init__.py
src/ollama_lab/client.py
src/ollama_lab/bench.py
tests/test_bench.py
tests/test_live.py
[project]
name = "ollama-lab"
version = "0.1.0"
requires-python = ">=3.10"
dependencies = ["ollama>=0.4"]
[project.optional-dependencies]
dev = ["pytest>=8"]
[tool.pytest.ini_options]
markers = ["live: needs a running Ollama daemon and a pulled model"]
client.py owns the transport seam, so the benchmark is testable:
"""A small layer over the daemon: metrics, and a transport you can fake."""
from __future__ import annotations
import json
import os
import urllib.error
import urllib.request
from dataclasses import dataclass
from typing import Any, Protocol
def daemon_url() -> str:
raw = os.environ.get("OLLAMA_HOST", "127.0.0.1:11434").strip()
if "://" not in raw:
raw = "http://" + raw
return raw.rstrip("/")
class Transport(Protocol):
def post(self, path: str, payload: dict[str, Any], *, timeout: float) -> dict[str, Any]: ...
class HttpTransport:
def __init__(self, host: str | None = None, timeout: float = 120.0) -> None:
self.host = (host or daemon_url()).rstrip("/")
self.timeout = timeout
def post(self, path: str, payload: dict[str, Any], *, timeout: float | None = None) -> dict[str, Any]:
request = urllib.request.Request(
self.host + path,
data=json.dumps(payload).encode("utf-8"),
headers={"content-type": "application/json"},
method="POST",
)
try:
with urllib.request.urlopen(request, timeout=timeout or self.timeout) as response:
return json.loads(response.read().decode("utf-8"))
except urllib.error.HTTPError as error:
detail = error.read().decode("utf-8", "replace")
raise RuntimeError(f"HTTP {error.code} from {path}: {detail}") from error
@dataclass(frozen=True)
class Metrics:
eval_count: int | None
eval_duration_ns: int | None
prompt_eval_count: int | None
prompt_eval_duration_ns: int | None
load_duration_ns: int | None
total_duration_ns: int | None
done_reason: str
@classmethod
def from_response(cls, body: dict[str, Any]) -> "Metrics":
return cls(
eval_count=body.get("eval_count"),
eval_duration_ns=body.get("eval_duration"),
prompt_eval_count=body.get("prompt_eval_count"),
prompt_eval_duration_ns=body.get("prompt_eval_duration"),
load_duration_ns=body.get("load_duration"),
total_duration_ns=body.get("total_duration"),
done_reason=str(body.get("done_reason", "")),
)
@property
def tokens_per_second(self) -> float | None:
if not self.eval_count or not self.eval_duration_ns:
return None
return self.eval_count / (self.eval_duration_ns / 1e9)
@property
def seconds_to_first_token(self) -> float | None:
if self.load_duration_ns is None and self.prompt_eval_duration_ns is None:
return None
return ((self.load_duration_ns or 0) + (self.prompt_eval_duration_ns or 0)) / 1e9
class OllamaClient:
"""generate() over a Transport; stream=False keeps the measurement whole."""
def __init__(self, transport: Transport | None = None) -> None:
self.transport = transport or HttpTransport()
def generate(self, model: str, prompt: str, **options: Any) -> tuple[str, Metrics]:
body = self.transport.post(
"/api/generate",
{"model": model, "prompt": prompt, "stream": False, "options": options},
timeout=300.0,
)
return str(body.get("response", "")), Metrics.from_response(body)
bench.py holds the statistics:
"""Median, p95 and spread: the only summary a small sample can carry."""
from __future__ import annotations
import math
import statistics
from .client import Metrics, OllamaClient
def nearest_rank(values: list[float], pct: float) -> float | None:
"""The smallest value at or above the rank - p95 of 5 is one of the 5."""
ordered = sorted(v for v in values if math.isfinite(v))
if not ordered:
return None
rank = math.ceil((pct / 100) * len(ordered))
return ordered[max(0, min(len(ordered) - 1, rank - 1))]
def summarize(values: list[float]) -> dict[str, float | int | None]:
sample = [v for v in values if math.isfinite(v)]
if not sample:
return {"count": 0, "median": None, "p95": None, "spread": None}
mean = statistics.fmean(sample)
stdev = statistics.pstdev(sample) if len(sample) > 1 else 0.0
return {
"count": len(sample),
"median": statistics.median(sample),
"p95": nearest_rank(sample, 95),
"spread": (stdev / mean) if mean else None,
}
def bench(model: str, prompt: str, runs: int = 5, client: OllamaClient | None = None) -> dict:
"""runs measured calls after an unload, so run 1 is genuinely cold."""
if runs < 3:
raise ValueError("a benchmark needs at least 3 runs; one run is not a measurement")
client = client or OllamaClient()
client.transport.post("/api/generate", {"model": model, "keep_alive": 0}, timeout=30.0)
measured: list[Metrics] = []
for _ in range(runs):
_text, metrics = client.generate(model, prompt, num_predict=128, temperature=0, seed=42)
if metrics.done_reason == "length":
raise RuntimeError("the answer was cut by num_predict; raise it for a benchmark")
if metrics.tokens_per_second is None:
raise RuntimeError("the daemon reported no eval_duration; the rate is unknown")
measured.append(metrics)
return {
"model": model,
"runs": runs,
"tokens_per_second": summarize([m.tokens_per_second or 0.0 for m in measured]),
"seconds_to_first_token": summarize([m.seconds_to_first_token or 0.0 for m in measured]),
"cold_load_ns": measured[0].load_duration_ns,
}
tests/test_bench.py proves the arithmetic without a model: a fake transport
returns canned bodies with fixed nanosecond durations, so 100 tokens in 2 s is
exactly 50 tok/s.
"""The arithmetic, against a fake transport - no daemon, no model, no GPU."""
from __future__ import annotations
import pytest
from ollama_lab.bench import bench, nearest_rank, summarize
from ollama_lab.client import Metrics, OllamaClient
class FakeTransport:
def __init__(self, responses: list[dict]) -> None:
self.responses = list(responses)
self.calls: list[tuple[str, dict]] = []
def post(self, path: str, payload: dict, *, timeout: float) -> dict:
self.calls.append((path, payload))
return self.responses.pop(0)
def body(eval_duration: int, load_duration: int = 0) -> dict:
return {
"response": "ok",
"done": True,
"done_reason": "stop",
"eval_count": 100,
"eval_duration": eval_duration,
"prompt_eval_count": 20,
"prompt_eval_duration": 200_000_000,
"load_duration": load_duration,
"total_duration": eval_duration + 200_000_000 + load_duration,
}
def test_rate_is_the_eval_pair() -> None:
metrics = Metrics.from_response(body(2_000_000_000))
assert metrics.tokens_per_second == 50
assert metrics.seconds_to_first_token == pytest.approx(0.2)
def test_a_zero_duration_is_unknown_not_infinite() -> None:
metrics = Metrics.from_response({**body(0), "eval_count": 0})
assert metrics.tokens_per_second is None
def test_p95_is_a_real_run() -> None:
assert nearest_rank([50, 40, 66.7, 33.3, 100], 95) == 100
assert nearest_rank([], 95) is None
assert summarize([7])["spread"] == 0
def test_bench_unloads_then_measures() -> None:
transport = FakeTransport([{"done": True}] + [body(2_000_000_000 + index) for index in range(3)])
result = bench("test:1b", "bench", runs=3, client=OllamaClient(transport))
assert transport.calls[0][1]["keep_alive"] == 0
assert result["runs"] == 3
assert result["tokens_per_second"]["median"] == pytest.approx(50)
def test_a_short_sample_is_refused() -> None:
with pytest.raises(ValueError):
bench("test:1b", "bench", runs=2)
tests/test_live.py is the half that needs a machine: it skips unless a daemon
answers AND a completion model is pulled, and says which condition failed.
"""The live half: skipped loudly when there is no daemon or no model."""
from __future__ import annotations
import json
import urllib.request
import pytest
from ollama_lab.client import daemon_url
def daemon_version() -> str | None:
try:
with urllib.request.urlopen(daemon_url() + "/api/version", timeout=1.5) as response:
return json.loads(response.read().decode("utf-8")).get("version")
except Exception:
return None
def smallest_model() -> str | None:
try:
with urllib.request.urlopen(daemon_url() + "/api/tags", timeout=2.0) as response:
models = json.loads(response.read().decode("utf-8")).get("models", [])
except Exception:
return None
candidates = [m for m in models if "embed" not in str(m.get("name", ""))]
if not candidates:
return None
return sorted(candidates, key=lambda m: m.get("size", 0))[0]["name"]
VERSION = daemon_version()
MODEL = smallest_model() if VERSION is not None else None
pytestmark = pytest.mark.skipif(
VERSION is None or MODEL is None,
reason=f"no live Ollama (version={VERSION}, model={MODEL}); start one with `ollama serve` and pull a model",
)
def test_one_tiny_generation() -> None:
from ollama_lab.client import OllamaClient
text, metrics = OllamaClient().generate(MODEL, "Reply with the single word: ok", num_predict=8, temperature=0)
assert text.strip() != ""
assert metrics.eval_count is not None and metrics.eval_count >= 0
Run pytest -m "not live" in CI and pytest -m live on a machine with the
daemon. The rule the fake transport encodes is the important one: the half
that needs a model is never the half that proves your arithmetic.
eval_duration as milliseconds. It is nanoseconds; 2e9 is 2 s.client.chat(stream=True) as a string. It is a generator of
parts, and only the last part has the timings.OLLAMA_HOST is a connect address. The daemon may bind
0.0.0.0; a client should use 127.0.0.1.load_duration
is 0 for every run - the cold number you meant to report does not exist.temperature at its default. Sampling changes
eval_count; fix temperature=0 and seed.format= schema blindly. Validate the JSON; a model can still
return a shape that parses but does not match.还没有评价。欢迎分享你的使用感受。
无需登录即可发表。欢迎分享使用感受、提问,或说明在哪些环境下无法运行。
还没有评论。来写第一条吧。
Author an editorial HTML+SVG diagram with the Diagram Design kit - choose the visual type, read its reference, start from an example, and satisfy the safety and accessibility contract that diagram_write({kind:"html"}) enforces.
中文简介正在准备中。当前显示原文说明。
Control this machine's Docker or Podman engine through the docker_* tools - read the engine's state first, list containers and images, inspect one, tail its logs, pull an image and start it, then stop, restart or remove what you started. Covers the engine that is asleep behind Docker Desktop and the exact call for each verb, the confirm rule removals obey, and the flags the tools deliberately do not have.
中文简介正在准备中。当前显示原文说明。
Drive ffmpeg through the media_run tool as an argv array - remux instead of re-encode, map streams explicitly, trim at keyframes, scale and crop, extract audio and frames, and build GIFs, contact sheets and concatenations. Covers the flags this pack injects, reading a failure from its exit code and last log lines, and the version-dependent spellings to hedge.
中文简介正在准备中。当前显示原文说明。
Read a media file's real structure with ffprobe through media_run - the JSON invocation and how to read it, what every family of field means, how to count frames, sample without decoding everything, detect HDR and interlacing, and diagnose a missing duration, a stream with no frames or a container that lies. Covers when media_probe's own report is already enough.
中文简介正在准备中。当前显示原文说明。
Use when the user wants to design, redesign, shape, critique, audit, polish, simplify, harden, animate, or otherwise improve a frontend interface: websites, landing pages, dashboards, product UI, components, forms, settings, onboarding, empty states, and native iOS or Android screens. Handles UX review, hierarchy, layout, typography, color, motion, UX copy, accessibility, responsive behavior, edge cases, and capturing or documenting design systems (DESIGN.md, tokens). Also for making bland UI bolder, loud UI quieter, or ambitious visual effects. Not for backend-only or non-UI tasks.
中文简介正在准备中。当前显示原文说明。
Author Mermaid diagrams that parse the first time: choose the right diagram type for the question, write syntax the parser accepts, and keep the picture readable in a narrow panel. A bundled reference covers complex, large diagrams.
中文简介正在准备中。当前显示原文说明。