nitpick-core 0.1.0__tar.gz
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.
- nitpick_core-0.1.0/PKG-INFO +363 -0
- nitpick_core-0.1.0/README.md +335 -0
- nitpick_core-0.1.0/nitpick_core/__init__.py +51 -0
- nitpick_core-0.1.0/nitpick_core/__main__.py +6 -0
- nitpick_core-0.1.0/nitpick_core/api.py +298 -0
- nitpick_core-0.1.0/nitpick_core/builder.py +292 -0
- nitpick_core-0.1.0/nitpick_core/cli.py +151 -0
- nitpick_core-0.1.0/nitpick_core/client.py +155 -0
- nitpick_core-0.1.0/nitpick_core/config.py +102 -0
- nitpick_core-0.1.0/nitpick_core/errors.py +40 -0
- nitpick_core-0.1.0/nitpick_core/flows.py +213 -0
- nitpick_core-0.1.0/nitpick_core/registry.py +252 -0
- nitpick_core-0.1.0/nitpick_core/runtime.py +327 -0
- nitpick_core-0.1.0/nitpick_core.egg-info/PKG-INFO +363 -0
- nitpick_core-0.1.0/nitpick_core.egg-info/SOURCES.txt +25 -0
- nitpick_core-0.1.0/nitpick_core.egg-info/dependency_links.txt +1 -0
- nitpick_core-0.1.0/nitpick_core.egg-info/entry_points.txt +2 -0
- nitpick_core-0.1.0/nitpick_core.egg-info/requires.txt +12 -0
- nitpick_core-0.1.0/nitpick_core.egg-info/top_level.txt +1 -0
- nitpick_core-0.1.0/pyproject.toml +55 -0
- nitpick_core-0.1.0/setup.cfg +4 -0
- nitpick_core-0.1.0/tests/test_api.py +180 -0
- nitpick_core-0.1.0/tests/test_builder.py +155 -0
- nitpick_core-0.1.0/tests/test_client.py +151 -0
- nitpick_core-0.1.0/tests/test_flows.py +85 -0
- nitpick_core-0.1.0/tests/test_registry.py +169 -0
- nitpick_core-0.1.0/tests/test_runtime.py +135 -0
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nitpick-core
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: The agent runtime for nitpick — loads agent definitions from minidb, dispatches them through Prefect, and serves voice rooms.
|
|
5
|
+
Author: Arsh0922
|
|
6
|
+
License: BSD-2-Clause
|
|
7
|
+
Keywords: voice,agent,prefect,minidb,webrtc,orchestration
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
Requires-Dist: nitpick-voice>=0.2.0
|
|
18
|
+
Requires-Dist: minidb-db>=0.7.0
|
|
19
|
+
Requires-Dist: prefect>=3.0
|
|
20
|
+
Requires-Dist: httpx>=0.26
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
23
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
24
|
+
Requires-Dist: pytest-timeout>=2.3; extra == "dev"
|
|
25
|
+
Requires-Dist: build>=1.2; extra == "dev"
|
|
26
|
+
Requires-Dist: twine>=6.0; extra == "dev"
|
|
27
|
+
Requires-Dist: pyflakes>=3.0; extra == "dev"
|
|
28
|
+
|
|
29
|
+
# nitpick-core
|
|
30
|
+
|
|
31
|
+
**The agent runtime for [nitpick](https://github.com/V1PASH/nitpick).**
|
|
32
|
+
|
|
33
|
+
Loads agent definitions from a [minidb](https://github.com/V1PASH/minidb)
|
|
34
|
+
registry, dispatches them through [Prefect](https://docs.prefect.io), and
|
|
35
|
+
serves the voice rooms so people can talk to them.
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
Django nitpick-core Prefect
|
|
39
|
+
voice_service
|
|
40
|
+
│ │ │
|
|
41
|
+
│ POST /api/tasks │ │
|
|
42
|
+
├─────────────────────────▶│ │
|
|
43
|
+
│ │ run the flow │
|
|
44
|
+
│ ├────────────────────────────▶│
|
|
45
|
+
│ │ │
|
|
46
|
+
│ │ dispatch ──▶ agent runtime│
|
|
47
|
+
│ GET /api/sessions/{id} │ │
|
|
48
|
+
│◀─────────────────────────│ │
|
|
49
|
+
│ │ │
|
|
50
|
+
│ │ WS /ws │
|
|
51
|
+
│ │◀──── browser / SDK ────────▶│
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Prefect **dispatches**; the runtime **processes**. A voice session is
|
|
55
|
+
long-running and interactive, which does not fit Prefect's bounded-task
|
|
56
|
+
model — so the flow is short (it hands the request over and returns a
|
|
57
|
+
ticket) and the session itself is owned by the runtime, which can hold
|
|
58
|
+
many at once.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Quick start
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
git clone https://github.com/V1PASH/nitpick-core.git
|
|
66
|
+
cd nitpick-core
|
|
67
|
+
python3 -m venv .venv && source .venv/bin/activate
|
|
68
|
+
pip install -e ".[dev]"
|
|
69
|
+
|
|
70
|
+
# Point it at the registry the Django service writes to.
|
|
71
|
+
export MINIDB_PATH=/path/to/voice_service/voice.db
|
|
72
|
+
export OPENAI_API_KEY=sk-...
|
|
73
|
+
|
|
74
|
+
nitpick-core serve --port 8100
|
|
75
|
+
```
|
|
76
|
+
```bash
|
|
77
|
+
# What is in the registry?
|
|
78
|
+
nitpick-core agents
|
|
79
|
+
|
|
80
|
+
# Queue an agent by hand.
|
|
81
|
+
nitpick-core dispatch acme-support --room demo
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Open <http://localhost:8100> and talk to it.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## The registry
|
|
89
|
+
|
|
90
|
+
The agent definitions live in the same minidb database the Django service
|
|
91
|
+
writes to — two collections:
|
|
92
|
+
|
|
93
|
+
`agent`
|
|
94
|
+
: the agents. A name, a prompt, and the three model slugs it is wired
|
|
95
|
+
together from.
|
|
96
|
+
|
|
97
|
+
`models`
|
|
98
|
+
: the registry of models an agent can be built from. Each has a `kind`
|
|
99
|
+
(`llm`, `tts` or `stt`) and a `handle` naming the provider.
|
|
100
|
+
|
|
101
|
+
Every document carries a `handle` — a stable identifier. nitpick-core
|
|
102
|
+
fetches an agent **by its handle**:
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from nitpick_core import resolve_agent
|
|
106
|
+
|
|
107
|
+
resolved = resolve_agent("acme-support")
|
|
108
|
+
resolved["agent"]["prompt"] # the system prompt
|
|
109
|
+
resolved["llm"]["slug"] # 'gpt-4o'
|
|
110
|
+
resolved["stt"]["slug"] # 'whisper'
|
|
111
|
+
resolved["tts"]["slug"] # 'nova'
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### How a document becomes a running agent
|
|
115
|
+
|
|
116
|
+
The registry stores *what* an agent is, in a provider-neutral way. The
|
|
117
|
+
builder translates that into the provider-specific strings nitpick's
|
|
118
|
+
`Agent` facade expects, using the model's `handle`:
|
|
119
|
+
|
|
120
|
+
| Registry | nitpick | Derived from |
|
|
121
|
+
|---|---|---|
|
|
122
|
+
| `llm` model | `llm="openai:<model>"` | the provider handle, and the model's slug or `config["model"]` |
|
|
123
|
+
| `stt` model | `stt="openai" \| "deepgram" \| "sarvam"` | the provider handle |
|
|
124
|
+
| `tts` model | `tts="openai:<voice>"` | the provider handle, and `config["voice"]` or the first of `config["voices"]` |
|
|
125
|
+
|
|
126
|
+
A model whose provider nitpick does not know about is still usable if it
|
|
127
|
+
declares an OpenAI-compatible `base_url` — that is how a self-hosted model
|
|
128
|
+
or a gateway can be reached.
|
|
129
|
+
|
|
130
|
+
The agent's own `config` may carry pipeline settings:
|
|
131
|
+
|
|
132
|
+
```json
|
|
133
|
+
{
|
|
134
|
+
"language": "en",
|
|
135
|
+
"vad": "silero",
|
|
136
|
+
"allow_interruptions": true
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## The client
|
|
143
|
+
|
|
144
|
+
A typed async client, so another service can dispatch agents with one
|
|
145
|
+
import:
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
from nitpick_core import CoreClient
|
|
149
|
+
|
|
150
|
+
async with CoreClient("http://127.0.0.1:8100") as client:
|
|
151
|
+
session = await client.dispatch("acme-support", room_id="demo")
|
|
152
|
+
print(session["id"], session["status"])
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
| Method | Purpose |
|
|
156
|
+
|---|---|
|
|
157
|
+
| `dispatch(handle, *, room_id=None, overrides=None, metadata=None)` | Queue an agent to run |
|
|
158
|
+
| `session(session_id)` | One session |
|
|
159
|
+
| `sessions()` | Every session |
|
|
160
|
+
| `agents()` | The registry the service is running from |
|
|
161
|
+
| `agent(handle)` | One agent, with its models resolved |
|
|
162
|
+
| `health()` | Is the service up? |
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## The HTTP API
|
|
167
|
+
|
|
168
|
+
### `POST /tasks` — dispatch a task
|
|
169
|
+
|
|
170
|
+
```json
|
|
171
|
+
{ "handle": "acme-support", "room_id": "demo", "metadata": {"campaign": "spring"} }
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
```json
|
|
175
|
+
{
|
|
176
|
+
"id": "ses-3d248ab4429c",
|
|
177
|
+
"handle": "acme-support",
|
|
178
|
+
"room_id": "demo",
|
|
179
|
+
"status": "queued",
|
|
180
|
+
"flow_run_id": "28c24f9f-7434-4423-aae2-0a7fcace9aa7",
|
|
181
|
+
"agent": { "stt": "openai", "llm": "openai:gpt-4o", "tts": "openai:nova", ... }
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
The agent is resolved **before** the task is queued, so an unknown handle
|
|
186
|
+
comes back as a `404` straight away rather than a session that quietly
|
|
187
|
+
fails later.
|
|
188
|
+
|
|
189
|
+
| Status | Meaning |
|
|
190
|
+
|---|---|
|
|
191
|
+
| `202` | Queued |
|
|
192
|
+
| `400` | The task could not be dispatched |
|
|
193
|
+
| `404` | No such agent or model |
|
|
194
|
+
| `422` | The agent cannot be built (an unsupported provider, for example) |
|
|
195
|
+
| `503` | The registry could not be read |
|
|
196
|
+
|
|
197
|
+
### `GET /tasks/{id}` and `GET /sessions/{id}` — the session
|
|
198
|
+
|
|
199
|
+
```json
|
|
200
|
+
{
|
|
201
|
+
"id": "ses-3d248ab4429c",
|
|
202
|
+
"status": "running",
|
|
203
|
+
"started_at": 1763000000.0,
|
|
204
|
+
"duration_seconds": 42.5,
|
|
205
|
+
"flow_run_id": "28c24f9f-..."
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Statuses: `queued` → `starting` → `waiting` (nobody has joined yet) →
|
|
210
|
+
`running` (somebody is talking) → `ended`, or `failed`.
|
|
211
|
+
|
|
212
|
+
### `GET /agents` and `GET /agents/{handle}` — the registry
|
|
213
|
+
|
|
214
|
+
`GET /agents` lists every agent. `GET /agents/{handle}` returns the agent
|
|
215
|
+
with the models it names resolved.
|
|
216
|
+
|
|
217
|
+
### `GET /health`
|
|
218
|
+
|
|
219
|
+
### `WS /ws` — the signaling socket
|
|
220
|
+
|
|
221
|
+
The nitpick room protocol, so somebody can talk to a running agent. See
|
|
222
|
+
[nitpick's SDK documentation](https://github.com/V1PASH/nitpick/blob/main/docs/sdk.md).
|
|
223
|
+
|
|
224
|
+
### `GET /` — the browser client
|
|
225
|
+
|
|
226
|
+
nitpick's own client, served from the installed package.
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## Prefect
|
|
231
|
+
|
|
232
|
+
The flow is `dispatch-agent-task`. It is registered **once** — registering
|
|
233
|
+
it per call would restart Prefect's ephemeral server each time, adding
|
|
234
|
+
seconds to every dispatch.
|
|
235
|
+
|
|
236
|
+
**Self-contained by default.** With no `PREFECT_API_URL`, Prefect runs an
|
|
237
|
+
ephemeral local server, so there is no external infrastructure to set up.
|
|
238
|
+
The first dispatch pays about three seconds for the server to start; after
|
|
239
|
+
that a dispatch takes about one second.
|
|
240
|
+
|
|
241
|
+
**External server.** Set `PREFECT_API_URL` and nitpick-core talks to that
|
|
242
|
+
instead:
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
export PREFECT_API_URL=http://prefect-server:4200/api
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Either way the session records its flow run id, so a conversation can be
|
|
249
|
+
traced back to the dispatch that started it.
|
|
250
|
+
|
|
251
|
+
`PREFECT_HOME` is resolved to an absolute path and exported, so the
|
|
252
|
+
ephemeral server subprocess finds the same state the service does — a
|
|
253
|
+
service should not depend on whose home directory it runs under.
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## Configuration
|
|
258
|
+
|
|
259
|
+
| Variable | Default | Purpose |
|
|
260
|
+
|---|---|---|
|
|
261
|
+
| `MINIDB_PATH` | `voice.db` | The registry database |
|
|
262
|
+
| `AGENT_COLLECTION` | `agent` | The collection the agents live in |
|
|
263
|
+
| `MODEL_COLLECTION` | `models` | The collection the models live in |
|
|
264
|
+
| `NITPICK_CORE_HOST` | `0.0.0.0` | The HTTP host |
|
|
265
|
+
| `NITPICK_CORE_PORT` | `8100` | The HTTP port |
|
|
266
|
+
| `NITPICK_CORE_MAX_SESSIONS` | `4` | How many sessions run at once |
|
|
267
|
+
| `NITPICK_CORE_SESSION_MAX_SECONDS` | `900` | The longest a session may live |
|
|
268
|
+
| `NITPICK_CORE_JOIN_TIMEOUT` | `120` | How long a session waits for somebody to join |
|
|
269
|
+
| `NITPICK_CORE_IDLE_TIMEOUT` | `30` | How long a session waits after everybody leaves |
|
|
270
|
+
| `NITPICK_CORE_DEFAULT_ROOM` | `demo` | The room a task joins when it does not name one |
|
|
271
|
+
| `NITPICK_CORE_WEB_ROOT` | nitpick's client | The browser client to serve |
|
|
272
|
+
| `PREFECT_API_URL` | unset | An external Prefect server |
|
|
273
|
+
| `PREFECT_HOME` | `.prefect` | Where Prefect keeps its state, relative to the working directory |
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
## Publishing
|
|
278
|
+
|
|
279
|
+
The package is built and published with [go-task](https://taskfile.dev):
|
|
280
|
+
|
|
281
|
+
```bash
|
|
282
|
+
task # list the tasks
|
|
283
|
+
task test # run the tests
|
|
284
|
+
task build # build the wheel and the sdist
|
|
285
|
+
task check # the pre-publish checks on their own
|
|
286
|
+
task publish # build, check and upload to PyPI
|
|
287
|
+
task publish-test # upload to TestPyPI instead
|
|
288
|
+
task release # tag the current version and push the tag
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
`task publish` refuses to run unless the working tree is clean, the version
|
|
292
|
+
in `pyproject.toml` matches the package's `__version__`, the version is not
|
|
293
|
+
already on PyPI, and the artefacts pass `twine check --strict`.
|
|
294
|
+
|
|
295
|
+
There is also a `publish` GitHub workflow that uses PyPI's **trusted
|
|
296
|
+
publishing**, so a release can be published from CI with no API token
|
|
297
|
+
stored anywhere.
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
## How a session runs
|
|
302
|
+
|
|
303
|
+
```
|
|
304
|
+
POST /tasks
|
|
305
|
+
│
|
|
306
|
+
▼
|
|
307
|
+
Prefect flow ──▶ runtime.dispatch()
|
|
308
|
+
│ │
|
|
309
|
+
│ ├─ resolve the agent from minidb ← fails fast
|
|
310
|
+
│ ├─ build the spec
|
|
311
|
+
│ └─ queue it
|
|
312
|
+
│
|
|
313
|
+
▼ returns a ticket
|
|
314
|
+
│
|
|
315
|
+
a worker picks it up
|
|
316
|
+
│
|
|
317
|
+
├─ build the nitpick Agent
|
|
318
|
+
├─ join the room
|
|
319
|
+
├─ start the pipeline
|
|
320
|
+
│
|
|
321
|
+
the session lives until:
|
|
322
|
+
├─ the pipeline ends
|
|
323
|
+
├─ everybody leaves (idle timeout)
|
|
324
|
+
├─ nobody joins (join timeout)
|
|
325
|
+
└─ it hits its maximum duration
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
The agent is a participant in the room, so a browser (or the nitpick SDK)
|
|
329
|
+
can join and talk to it exactly as it would talk to any other peer.
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
## Tests
|
|
334
|
+
|
|
335
|
+
```bash
|
|
336
|
+
pytest # everything
|
|
337
|
+
pytest -m "not slow" # skip the tests that start a Prefect server
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
42 tests covering the registry lookups, the document-to-agent translation,
|
|
341
|
+
the dispatch queue and session lifecycle, the HTTP surface, and the Prefect
|
|
342
|
+
flow.
|
|
343
|
+
|
|
344
|
+
---
|
|
345
|
+
|
|
346
|
+
## Notes
|
|
347
|
+
|
|
348
|
+
* **The framework is on PyPI as `nitpick-voice`.** The name `nitpick` on
|
|
349
|
+
PyPI belongs to an unrelated package, so the distribution was renamed.
|
|
350
|
+
**The import name is still `nitpick`** — `from nitpick import Agent`.
|
|
351
|
+
* **Providers.** nitpick speaks the OpenAI wire format, plus Deepgram and
|
|
352
|
+
Sarvam for speech-to-text. An Anthropic model needs a `base_url` in its
|
|
353
|
+
config, pointing at an OpenAI-compatible gateway.
|
|
354
|
+
* **The Django service is not modified.** nitpick-core reads the same
|
|
355
|
+
database file, so the two agree without a network hop. Pointing
|
|
356
|
+
`MINIDB_PATH` at the same file is the whole integration.
|
|
357
|
+
* **The rooms and signaling layer came from `project-0`.** That prototype
|
|
358
|
+
is archived; nitpick carries the architecture with several fixes applied
|
|
359
|
+
(see nitpick's `docs/architecture.md`).
|
|
360
|
+
|
|
361
|
+
## License
|
|
362
|
+
|
|
363
|
+
BSD-2-Clause.
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
# nitpick-core
|
|
2
|
+
|
|
3
|
+
**The agent runtime for [nitpick](https://github.com/V1PASH/nitpick).**
|
|
4
|
+
|
|
5
|
+
Loads agent definitions from a [minidb](https://github.com/V1PASH/minidb)
|
|
6
|
+
registry, dispatches them through [Prefect](https://docs.prefect.io), and
|
|
7
|
+
serves the voice rooms so people can talk to them.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
Django nitpick-core Prefect
|
|
11
|
+
voice_service
|
|
12
|
+
│ │ │
|
|
13
|
+
│ POST /api/tasks │ │
|
|
14
|
+
├─────────────────────────▶│ │
|
|
15
|
+
│ │ run the flow │
|
|
16
|
+
│ ├────────────────────────────▶│
|
|
17
|
+
│ │ │
|
|
18
|
+
│ │ dispatch ──▶ agent runtime│
|
|
19
|
+
│ GET /api/sessions/{id} │ │
|
|
20
|
+
│◀─────────────────────────│ │
|
|
21
|
+
│ │ │
|
|
22
|
+
│ │ WS /ws │
|
|
23
|
+
│ │◀──── browser / SDK ────────▶│
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Prefect **dispatches**; the runtime **processes**. A voice session is
|
|
27
|
+
long-running and interactive, which does not fit Prefect's bounded-task
|
|
28
|
+
model — so the flow is short (it hands the request over and returns a
|
|
29
|
+
ticket) and the session itself is owned by the runtime, which can hold
|
|
30
|
+
many at once.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Quick start
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
git clone https://github.com/V1PASH/nitpick-core.git
|
|
38
|
+
cd nitpick-core
|
|
39
|
+
python3 -m venv .venv && source .venv/bin/activate
|
|
40
|
+
pip install -e ".[dev]"
|
|
41
|
+
|
|
42
|
+
# Point it at the registry the Django service writes to.
|
|
43
|
+
export MINIDB_PATH=/path/to/voice_service/voice.db
|
|
44
|
+
export OPENAI_API_KEY=sk-...
|
|
45
|
+
|
|
46
|
+
nitpick-core serve --port 8100
|
|
47
|
+
```
|
|
48
|
+
```bash
|
|
49
|
+
# What is in the registry?
|
|
50
|
+
nitpick-core agents
|
|
51
|
+
|
|
52
|
+
# Queue an agent by hand.
|
|
53
|
+
nitpick-core dispatch acme-support --room demo
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Open <http://localhost:8100> and talk to it.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## The registry
|
|
61
|
+
|
|
62
|
+
The agent definitions live in the same minidb database the Django service
|
|
63
|
+
writes to — two collections:
|
|
64
|
+
|
|
65
|
+
`agent`
|
|
66
|
+
: the agents. A name, a prompt, and the three model slugs it is wired
|
|
67
|
+
together from.
|
|
68
|
+
|
|
69
|
+
`models`
|
|
70
|
+
: the registry of models an agent can be built from. Each has a `kind`
|
|
71
|
+
(`llm`, `tts` or `stt`) and a `handle` naming the provider.
|
|
72
|
+
|
|
73
|
+
Every document carries a `handle` — a stable identifier. nitpick-core
|
|
74
|
+
fetches an agent **by its handle**:
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from nitpick_core import resolve_agent
|
|
78
|
+
|
|
79
|
+
resolved = resolve_agent("acme-support")
|
|
80
|
+
resolved["agent"]["prompt"] # the system prompt
|
|
81
|
+
resolved["llm"]["slug"] # 'gpt-4o'
|
|
82
|
+
resolved["stt"]["slug"] # 'whisper'
|
|
83
|
+
resolved["tts"]["slug"] # 'nova'
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### How a document becomes a running agent
|
|
87
|
+
|
|
88
|
+
The registry stores *what* an agent is, in a provider-neutral way. The
|
|
89
|
+
builder translates that into the provider-specific strings nitpick's
|
|
90
|
+
`Agent` facade expects, using the model's `handle`:
|
|
91
|
+
|
|
92
|
+
| Registry | nitpick | Derived from |
|
|
93
|
+
|---|---|---|
|
|
94
|
+
| `llm` model | `llm="openai:<model>"` | the provider handle, and the model's slug or `config["model"]` |
|
|
95
|
+
| `stt` model | `stt="openai" \| "deepgram" \| "sarvam"` | the provider handle |
|
|
96
|
+
| `tts` model | `tts="openai:<voice>"` | the provider handle, and `config["voice"]` or the first of `config["voices"]` |
|
|
97
|
+
|
|
98
|
+
A model whose provider nitpick does not know about is still usable if it
|
|
99
|
+
declares an OpenAI-compatible `base_url` — that is how a self-hosted model
|
|
100
|
+
or a gateway can be reached.
|
|
101
|
+
|
|
102
|
+
The agent's own `config` may carry pipeline settings:
|
|
103
|
+
|
|
104
|
+
```json
|
|
105
|
+
{
|
|
106
|
+
"language": "en",
|
|
107
|
+
"vad": "silero",
|
|
108
|
+
"allow_interruptions": true
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## The client
|
|
115
|
+
|
|
116
|
+
A typed async client, so another service can dispatch agents with one
|
|
117
|
+
import:
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
from nitpick_core import CoreClient
|
|
121
|
+
|
|
122
|
+
async with CoreClient("http://127.0.0.1:8100") as client:
|
|
123
|
+
session = await client.dispatch("acme-support", room_id="demo")
|
|
124
|
+
print(session["id"], session["status"])
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
| Method | Purpose |
|
|
128
|
+
|---|---|
|
|
129
|
+
| `dispatch(handle, *, room_id=None, overrides=None, metadata=None)` | Queue an agent to run |
|
|
130
|
+
| `session(session_id)` | One session |
|
|
131
|
+
| `sessions()` | Every session |
|
|
132
|
+
| `agents()` | The registry the service is running from |
|
|
133
|
+
| `agent(handle)` | One agent, with its models resolved |
|
|
134
|
+
| `health()` | Is the service up? |
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## The HTTP API
|
|
139
|
+
|
|
140
|
+
### `POST /tasks` — dispatch a task
|
|
141
|
+
|
|
142
|
+
```json
|
|
143
|
+
{ "handle": "acme-support", "room_id": "demo", "metadata": {"campaign": "spring"} }
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
```json
|
|
147
|
+
{
|
|
148
|
+
"id": "ses-3d248ab4429c",
|
|
149
|
+
"handle": "acme-support",
|
|
150
|
+
"room_id": "demo",
|
|
151
|
+
"status": "queued",
|
|
152
|
+
"flow_run_id": "28c24f9f-7434-4423-aae2-0a7fcace9aa7",
|
|
153
|
+
"agent": { "stt": "openai", "llm": "openai:gpt-4o", "tts": "openai:nova", ... }
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
The agent is resolved **before** the task is queued, so an unknown handle
|
|
158
|
+
comes back as a `404` straight away rather than a session that quietly
|
|
159
|
+
fails later.
|
|
160
|
+
|
|
161
|
+
| Status | Meaning |
|
|
162
|
+
|---|---|
|
|
163
|
+
| `202` | Queued |
|
|
164
|
+
| `400` | The task could not be dispatched |
|
|
165
|
+
| `404` | No such agent or model |
|
|
166
|
+
| `422` | The agent cannot be built (an unsupported provider, for example) |
|
|
167
|
+
| `503` | The registry could not be read |
|
|
168
|
+
|
|
169
|
+
### `GET /tasks/{id}` and `GET /sessions/{id}` — the session
|
|
170
|
+
|
|
171
|
+
```json
|
|
172
|
+
{
|
|
173
|
+
"id": "ses-3d248ab4429c",
|
|
174
|
+
"status": "running",
|
|
175
|
+
"started_at": 1763000000.0,
|
|
176
|
+
"duration_seconds": 42.5,
|
|
177
|
+
"flow_run_id": "28c24f9f-..."
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Statuses: `queued` → `starting` → `waiting` (nobody has joined yet) →
|
|
182
|
+
`running` (somebody is talking) → `ended`, or `failed`.
|
|
183
|
+
|
|
184
|
+
### `GET /agents` and `GET /agents/{handle}` — the registry
|
|
185
|
+
|
|
186
|
+
`GET /agents` lists every agent. `GET /agents/{handle}` returns the agent
|
|
187
|
+
with the models it names resolved.
|
|
188
|
+
|
|
189
|
+
### `GET /health`
|
|
190
|
+
|
|
191
|
+
### `WS /ws` — the signaling socket
|
|
192
|
+
|
|
193
|
+
The nitpick room protocol, so somebody can talk to a running agent. See
|
|
194
|
+
[nitpick's SDK documentation](https://github.com/V1PASH/nitpick/blob/main/docs/sdk.md).
|
|
195
|
+
|
|
196
|
+
### `GET /` — the browser client
|
|
197
|
+
|
|
198
|
+
nitpick's own client, served from the installed package.
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## Prefect
|
|
203
|
+
|
|
204
|
+
The flow is `dispatch-agent-task`. It is registered **once** — registering
|
|
205
|
+
it per call would restart Prefect's ephemeral server each time, adding
|
|
206
|
+
seconds to every dispatch.
|
|
207
|
+
|
|
208
|
+
**Self-contained by default.** With no `PREFECT_API_URL`, Prefect runs an
|
|
209
|
+
ephemeral local server, so there is no external infrastructure to set up.
|
|
210
|
+
The first dispatch pays about three seconds for the server to start; after
|
|
211
|
+
that a dispatch takes about one second.
|
|
212
|
+
|
|
213
|
+
**External server.** Set `PREFECT_API_URL` and nitpick-core talks to that
|
|
214
|
+
instead:
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
export PREFECT_API_URL=http://prefect-server:4200/api
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Either way the session records its flow run id, so a conversation can be
|
|
221
|
+
traced back to the dispatch that started it.
|
|
222
|
+
|
|
223
|
+
`PREFECT_HOME` is resolved to an absolute path and exported, so the
|
|
224
|
+
ephemeral server subprocess finds the same state the service does — a
|
|
225
|
+
service should not depend on whose home directory it runs under.
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## Configuration
|
|
230
|
+
|
|
231
|
+
| Variable | Default | Purpose |
|
|
232
|
+
|---|---|---|
|
|
233
|
+
| `MINIDB_PATH` | `voice.db` | The registry database |
|
|
234
|
+
| `AGENT_COLLECTION` | `agent` | The collection the agents live in |
|
|
235
|
+
| `MODEL_COLLECTION` | `models` | The collection the models live in |
|
|
236
|
+
| `NITPICK_CORE_HOST` | `0.0.0.0` | The HTTP host |
|
|
237
|
+
| `NITPICK_CORE_PORT` | `8100` | The HTTP port |
|
|
238
|
+
| `NITPICK_CORE_MAX_SESSIONS` | `4` | How many sessions run at once |
|
|
239
|
+
| `NITPICK_CORE_SESSION_MAX_SECONDS` | `900` | The longest a session may live |
|
|
240
|
+
| `NITPICK_CORE_JOIN_TIMEOUT` | `120` | How long a session waits for somebody to join |
|
|
241
|
+
| `NITPICK_CORE_IDLE_TIMEOUT` | `30` | How long a session waits after everybody leaves |
|
|
242
|
+
| `NITPICK_CORE_DEFAULT_ROOM` | `demo` | The room a task joins when it does not name one |
|
|
243
|
+
| `NITPICK_CORE_WEB_ROOT` | nitpick's client | The browser client to serve |
|
|
244
|
+
| `PREFECT_API_URL` | unset | An external Prefect server |
|
|
245
|
+
| `PREFECT_HOME` | `.prefect` | Where Prefect keeps its state, relative to the working directory |
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## Publishing
|
|
250
|
+
|
|
251
|
+
The package is built and published with [go-task](https://taskfile.dev):
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
task # list the tasks
|
|
255
|
+
task test # run the tests
|
|
256
|
+
task build # build the wheel and the sdist
|
|
257
|
+
task check # the pre-publish checks on their own
|
|
258
|
+
task publish # build, check and upload to PyPI
|
|
259
|
+
task publish-test # upload to TestPyPI instead
|
|
260
|
+
task release # tag the current version and push the tag
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
`task publish` refuses to run unless the working tree is clean, the version
|
|
264
|
+
in `pyproject.toml` matches the package's `__version__`, the version is not
|
|
265
|
+
already on PyPI, and the artefacts pass `twine check --strict`.
|
|
266
|
+
|
|
267
|
+
There is also a `publish` GitHub workflow that uses PyPI's **trusted
|
|
268
|
+
publishing**, so a release can be published from CI with no API token
|
|
269
|
+
stored anywhere.
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
## How a session runs
|
|
274
|
+
|
|
275
|
+
```
|
|
276
|
+
POST /tasks
|
|
277
|
+
│
|
|
278
|
+
▼
|
|
279
|
+
Prefect flow ──▶ runtime.dispatch()
|
|
280
|
+
│ │
|
|
281
|
+
│ ├─ resolve the agent from minidb ← fails fast
|
|
282
|
+
│ ├─ build the spec
|
|
283
|
+
│ └─ queue it
|
|
284
|
+
│
|
|
285
|
+
▼ returns a ticket
|
|
286
|
+
│
|
|
287
|
+
a worker picks it up
|
|
288
|
+
│
|
|
289
|
+
├─ build the nitpick Agent
|
|
290
|
+
├─ join the room
|
|
291
|
+
├─ start the pipeline
|
|
292
|
+
│
|
|
293
|
+
the session lives until:
|
|
294
|
+
├─ the pipeline ends
|
|
295
|
+
├─ everybody leaves (idle timeout)
|
|
296
|
+
├─ nobody joins (join timeout)
|
|
297
|
+
└─ it hits its maximum duration
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
The agent is a participant in the room, so a browser (or the nitpick SDK)
|
|
301
|
+
can join and talk to it exactly as it would talk to any other peer.
|
|
302
|
+
|
|
303
|
+
---
|
|
304
|
+
|
|
305
|
+
## Tests
|
|
306
|
+
|
|
307
|
+
```bash
|
|
308
|
+
pytest # everything
|
|
309
|
+
pytest -m "not slow" # skip the tests that start a Prefect server
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
42 tests covering the registry lookups, the document-to-agent translation,
|
|
313
|
+
the dispatch queue and session lifecycle, the HTTP surface, and the Prefect
|
|
314
|
+
flow.
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
## Notes
|
|
319
|
+
|
|
320
|
+
* **The framework is on PyPI as `nitpick-voice`.** The name `nitpick` on
|
|
321
|
+
PyPI belongs to an unrelated package, so the distribution was renamed.
|
|
322
|
+
**The import name is still `nitpick`** — `from nitpick import Agent`.
|
|
323
|
+
* **Providers.** nitpick speaks the OpenAI wire format, plus Deepgram and
|
|
324
|
+
Sarvam for speech-to-text. An Anthropic model needs a `base_url` in its
|
|
325
|
+
config, pointing at an OpenAI-compatible gateway.
|
|
326
|
+
* **The Django service is not modified.** nitpick-core reads the same
|
|
327
|
+
database file, so the two agree without a network hop. Pointing
|
|
328
|
+
`MINIDB_PATH` at the same file is the whole integration.
|
|
329
|
+
* **The rooms and signaling layer came from `project-0`.** That prototype
|
|
330
|
+
is archived; nitpick carries the architecture with several fixes applied
|
|
331
|
+
(see nitpick's `docs/architecture.md`).
|
|
332
|
+
|
|
333
|
+
## License
|
|
334
|
+
|
|
335
|
+
BSD-2-Clause.
|