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.
@@ -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.