--- title: "Chat and Agents" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Chat and Agents} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include=FALSE, purl=FALSE} # Every chunk needs a GGUF model (and usually a GPU), so this vignette is # static: the code is shown but not run at build time. knitr::opts_chunk$set(eval = FALSE, purl = FALSE) ``` llamaR turns a local GGUF model into a chat backend for the R ecosystem. You can talk to it several ways, from lowest to highest level: * **HTTP server** — `llama_serve_openai()` exposes an OpenAI-compatible API any client can hit (OpenCode, the `openai` SDK, `curl`). * **Anthropic server** — `llama_serve_anthropic()` exposes an Anthropic Messages API so **Claude Code** runs against local inference (see section 7). * **ellmer `Chat`** — `chat_llamar()` returns an `ellmer::Chat`, so the whole ellmer / ragnar toolchain works against local inference. * **Command-line example** — `inst/examples/chat.R` wraps both for quick use. Both servers share a tool-aware chat layer (`llama_chat_build()` / `llama_chat_parse()`) so tool-calling models work end to end (section 6). ```{r, eval=FALSE, purl=FALSE} library(llamaR) ``` --- ## 1. The chat object: `chat_llamar()` `chat_llamar()` returns an [ellmer](https://ellmer.tidyverse.org/) `Chat`. It has two modes, picked by which argument you pass — the same DBI-style choice as `DBI::dbConnect()` (connection parameters *or* a ready connection). ### Mode A — spawn a server for a model Give it a model file and it starts `llama_serve_openai()` in a background process (via the **callr** package), waits for it to come up, and points a `Chat` at it. The server's lifetime is tied to the returned object: when it is garbage-collected (or R exits) the process is killed. ```{r, eval=FALSE, purl=FALSE} chat <- chat_llamar(model_path = "Ministral-3B-Instruct.gguf") chat$chat("Why is the sky blue?") chat_llamar_stop(chat) # stop the spawned server (or just let GC do it) ``` Large models can take a while to load from disk; raise `timeout` (default 180s) if a 14B at Q8 doesn't come up in time: ```{r, eval=FALSE, purl=FALSE} chat <- chat_llamar(model_path = "Qwen3-14B-Q8_0.gguf", timeout = 300) ``` ### Mode B — connect to a running server If you already run a server (in another process, or a pool of them), pass its URL. No process is spawned. ```{r, eval=FALSE, purl=FALSE} # In another process / shell: # llama_serve_openai("model.gguf", port = 11434L) chat <- chat_llamar(base_url = "http://127.0.0.1:11434/v1") chat$chat("Hello!") ``` ### System prompt ```{r, eval=FALSE, purl=FALSE} chat <- chat_llamar( model_path = "Ministral-3B-Instruct.gguf", system_prompt = "You are a concise assistant. Answer in one sentence." ) chat$chat("What is R?") ``` > **Under the hood.** `chat_llamar()` wraps `ellmer::chat_vllm()`, which talks > to the server's `/v1/chat/completions` endpoint — the de-facto standard our > server implements. (ellmer's `chat_openai()` targets OpenAI's newer > `/v1/responses` API, which the server does not implement.) --- ## 2. The server: `llama_serve_openai()` `chat_llamar(model_path=)` is a convenience wrapper; you can run the server directly for non-R clients. It needs the optional **drogonR** package for the HTTP/SSE layer. ```{r, eval=FALSE, purl=FALSE} llama_serve_openai("model.gguf", port = 11434L, n_ctx = 8192L) ``` It blocks, serving: * `GET /v1/models` * `POST /v1/chat/completions` (both blocking and `stream = true`) Point any OpenAI client at `http://127.0.0.1:11434/v1`: ```bash curl http://127.0.0.1:11434/v1/chat/completions \ -H 'Content-Type: application/json' \ -d '{"model":"model","messages":[{"role":"user","content":"Hello"}]}' ``` A runnable launcher lives at `inst/examples/serve_openai.R`. ### Connecting OpenCode Add an OpenAI-compatible provider in `opencode.json` (see the one in this repo) with `baseURL` set to `http://127.0.0.1:11434/v1` and the model id matching what `/v1/models` reports. --- ## 3. The command-line example `inst/examples/chat.R` wraps both modes for the terminal: ```bash # Spawn a server for the model and open an interactive prompt Rscript inst/examples/chat.R model.gguf # Positional [port] [n_ctx], plus flags Rscript inst/examples/chat.R model.gguf 11434 8192 \ --system "Be concise." --timeout 300 # One-shot: a trailing message prints a single reply and exits Rscript inst/examples/chat.R model.gguf "Why is the sky blue?" # Connect to a server you already started Rscript inst/examples/chat.R --url http://127.0.0.1:11434/v1 ``` In interactive mode, type a message and press Enter; a blank line or Ctrl-D quits. A spawned server is stopped automatically on exit. --- ## 4. ragnar: retrieval-augmented chat Because `chat_llamar()` returns a real `ellmer::Chat`, it plugs into [ragnar](https://ragnar.tidyverse.org/). Pair it with `embed_llamar()` (see `vignette("getting-started")`) for a fully local RAG stack: local embeddings for the store, local generation for the chat. ```{r, eval=FALSE, purl=FALSE} library(ragnar) store <- ragnar_store_create( location = "store.duckdb", embed = embed_llamar(model = "embedding-model.gguf") ) ragnar_store_insert(store, documents) ragnar_store_build_index(store) chat <- chat_llamar(model_path = "Ministral-3B-Instruct.gguf") ragnar_register_tool_retrieve(chat, store) chat$chat("What do the documents say about X?") ``` > **Note.** Tool calling is mediated by the chat protocol. Both servers emit > `tool_calls` (the tool-aware chat layer, see section 6), so a tool-calling > model can autonomously invoke the registered retrieve tool. Tool-calling > quality is model-dependent — capable models (e.g. Qwen3) drive retrieval > reliably; very small models may need manual retrieval. --- ## 5. Tool calling: `llama_chat_build()` / `llama_chat_parse()` The servers above call a lower-level, tool-aware chat layer you can also use directly. `llama_chat_build()` applies the model's chat template to messages **plus tool definitions** and returns everything needed to constrain and parse a tool call; `llama_chat_parse()` turns the raw output back into structured tool calls. ```{r, eval=FALSE, purl=FALSE} tools <- list(list( type = "function", "function" = list( name = "get_weather", description = "Get the current weather for a city.", parameters = list( type = "object", properties = list(city = list(type = "string")), required = list("city") ) ) )) messages <- list(list(role = "user", content = "What's the weather in Paris?")) built <- llama_chat_build(model, messages, tools = tools) # built$prompt — the formatted prompt to feed the model # built$grammar — grammar that constrains tool-call output # built$format — format id to pass to llama_chat_parse() # built$grammar_lazy, built$trigger_patterns, built$trigger_tokens, # built$additional_stops, built$preserved_tokens, built$parser ``` Generate against the returned prompt and grammar, then parse. Pass the lazy triggers **only when `built$grammar_lazy` is `TRUE`**: ```{r, eval=FALSE, purl=FALSE} lazy <- isTRUE(built$grammar_lazy) raw <- llama_generate( ctx, built$prompt, grammar = built$grammar, trigger_patterns = if (lazy) built$trigger_patterns, trigger_tokens = if (lazy) built$trigger_tokens ) parsed <- llama_chat_parse(raw, format = built$format, parser = built$parser) parsed$content # assistant text (may be empty for a pure tool call) parsed$tool_calls # data frame: name, arguments (JSON string), id ``` Tool-calling quality is model-dependent: capable models (e.g. Qwen3) emit clean tool calls; very small models are less reliable. --- ## 6. Claude Code on a local model: `llama_serve_anthropic()` `llama_serve_anthropic()` exposes an **Anthropic Messages API** so Claude Code (or any Anthropic SDK client) runs against local inference. It uses the tool-aware layer from section 5 and streams the Anthropic SSE event sequence. Like the OpenAI server it needs the optional **drogonR** package. ```{r, eval=FALSE, purl=FALSE} llama_serve_anthropic("model.gguf", port = 11435L, n_ctx = 32768L) ``` It serves `POST /v1/messages` and `GET /v1/models`. Point Claude Code at it via environment variables: ```bash export ANTHROPIC_BASE_URL=http://127.0.0.1:11435 export ANTHROPIC_API_KEY=local # any non-empty value claude ``` `enable_thinking` (default `FALSE`) toggles the chat template's reasoning mode for hybrid thinking models (Qwen3.5, etc.). It is off by default so Claude Code gets direct answers and fast tool calls; set it `TRUE` (and raise `max_tokens`) to keep the reasoning trace. A runnable launcher lives at `inst/examples/claude_code_launcher.sh`. --- ## 7. Concurrency The server is **single-sequence**: it handles one request at a time on the main R thread. That is enough for a single local user or agent. For parallel sessions, run a pool of servers on different ports and create one `chat_llamar(base_url=)` per worker — the worker-pool architecture is described in `TODO.md`. ```{r, eval=FALSE, purl=FALSE} ports <- c(11434L, 11435L, 11436L) chats <- lapply(ports, function(p) chat_llamar(base_url = sprintf("http://127.0.0.1:%d/v1", p))) ``` --- ## See also * `vignette("getting-started")` — the rest of the package. * `?chat_llamar`, `?llama_serve_openai`, `?llama_serve_anthropic` * `?llama_chat_build`, `?llama_chat_parse` * `inst/examples/chat.R`, `inst/examples/serve_openai.R`, `inst/examples/claude_code_launcher.sh`