feat: playbook tools, RAG, vision, voice, chat controls, faster boot

- Tool-using playbooks: read-only tool registry (search_memory,
  search_history, search_documents, list_models, get_time), per-playbook
  allowlist, agentic loop, and a "running tool" status indicator.
- Document ingest / RAG: documents table + chunker + embed/cosine retrieval
  reusing the existing stack; Documents page (upload/paste, viewer, delete);
  top-k chunks injected into the system prompt.
- Vision chat: attach images, base64 into /api/chat.
- Voice I/O: Web Speech dictation + read-aloud (browser-native, no backend).
- Chat controls: stop, regenerate, edit-and-resend; num_ctx knob in Settings.
- Per-playbook model override.
- History polish: per-conversation + bulk ShareGPT export.
- Faster ncp start: UI up first, Ollama warms in the background, with a
  per-phase timing readout.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
jon
2026-07-23 13:31:48 -05:00
co-authored by Claude Opus 4.8
parent f514d3dbe5
commit f509034fdd
15 changed files with 1061 additions and 96 deletions
+18 -6
View File
@@ -175,7 +175,7 @@ def _preferred_model(models: list, preference) -> str | None:
return None
def _chat_options(temperature: float | None, num_gpu: int | None) -> dict:
def _chat_options(temperature: float | None, num_gpu: int | None, num_ctx: int | None = None) -> dict:
"""Assemble the Ollama `options` block from the knobs we expose.
Returns an empty dict when nothing is set so callers can omit `options`
@@ -186,6 +186,8 @@ def _chat_options(temperature: float | None, num_gpu: int | None) -> dict:
opts["temperature"] = temperature
if num_gpu is not None:
opts["num_gpu"] = num_gpu
if num_ctx: # 0 / None -> let Ollama use the model default
opts["num_ctx"] = num_ctx
return opts
@@ -503,10 +505,16 @@ class OllamaManager:
temperature: float | None = None,
num_gpu: int | None = None,
think: bool = False,
tools: list | None = None,
num_ctx: int | None = None,
**kwargs,
):
"""Multi-turn chat via /api/chat (accepts a messages array with roles).
When `tools` is given (non-stream only), the request advertises them and
the FULL message dict is returned (so the caller sees `tool_calls`);
otherwise the response content string is returned as before.
`think` toggles Qwen3-style reasoning. Default off: the hidden <think>
block is pure latency for chat/memory. Ollama ignores it for models that
don't support thinking.
@@ -516,12 +524,14 @@ class OllamaManager:
if stream:
return self._chat_stream(
messages=messages, model=model, temperature=temperature,
num_gpu=num_gpu, think=think, start=start,
num_gpu=num_gpu, think=think, start=start, num_ctx=num_ctx,
)
else:
body: dict = {"model": model, "messages": messages, "stream": False}
body["think"] = think
opts = _chat_options(temperature, num_gpu)
if tools:
body["tools"] = tools
opts = _chat_options(temperature, num_gpu, num_ctx)
if opts:
body["options"] = opts
self._apply_keep_alive(body)
@@ -529,7 +539,9 @@ class OllamaManager:
r = await client.post(f"{self._api_base}/api/chat", json=body)
elapsed = time.perf_counter() - start
r.raise_for_status()
return r.json().get("message", {}).get("content", "")
message = r.json().get("message", {})
# Tool callers need the whole message (tool_calls); others want content.
return message if tools else message.get("content", "")
except Exception as e:
elapsed = time.perf_counter() - start
_log.exception("chat error after %.3fs: %s", elapsed, e)
@@ -643,12 +655,12 @@ class OllamaManager:
async def _chat_stream(self, messages: list, model: str, start: float,
temperature: float | None = None, num_gpu: int | None = None,
think: bool = False):
think: bool = False, num_ctx: int | None = None):
"""Async generator streaming tokens, then a final __meta__ stats sentinel."""
try:
body: dict = {"model": model, "messages": messages, "stream": True}
body["think"] = think # see chat(): reasoning off by default for speed
opts = _chat_options(temperature, num_gpu)
opts = _chat_options(temperature, num_gpu, num_ctx)
if opts:
body["options"] = opts
self._apply_keep_alive(body)