Skip to content

Commit 9499106

Browse files
committed
Update plugin
1 parent 1666daf commit 9499106

6 files changed

Lines changed: 164 additions & 13 deletions

File tree

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
22
"name": "docent",
3-
"version": "0.1.9",
3+
"version": "0.1.10",
44
"description": "Docent AI analysis tools"
55
}

plugins/docent/.mcp.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"docent": {
44
"type": "stdio",
55
"command": "uv",
6-
"args": ["tool", "run", "--from", "docent-python>=0.1.73", "docent-mcp"]
6+
"args": ["tool", "run", "--from", "docent-python>=0.1.74", "docent-mcp"]
77
}
88
}
99
}

plugins/docent/skills/docent/analysis.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ client = Docent.from_url("https://docent.transluce.org/dashboard/668354d8-...")
6464
```
6565
This parses the domain and collection ID from the URL automatically.
6666

67-
The Docent SDK can be configured by a `docent.env` file. The SDK searches from the current working directory upward through parent directories, then falls back to `~/.docent/docent.env` if no local file exists. You do not need to explicitly source `docent.env`. Config files may use INI-style `[section]` headers for multi-profile support; select a profile with `Docent(profile="my-profile")` or the `DOCENT_PROFILE` environment variable.
67+
The Docent SDK can be configured by a `docent.env` file. The default global config file is `~/.docent/docent.env`. The SDK also honors project-level `docent.env` files from the current working directory upward as local overrides. You do not need to explicitly source `docent.env`. Config files may use INI-style `[section]` headers for multi-profile support; select a profile with `Docent(profile="my-profile")` or the `DOCENT_PROFILE` environment variable.
6868

6969
If you're not sure what collection the user is talking about:
7070
* If the user provides a Docent dashboard URL (e.g., `https://docent.transluce.org/dashboard/668354d8-...`), use `Docent.from_url()` or extract the collection ID from the last path segment (the UUID).
@@ -80,7 +80,7 @@ If you run into any issues or unexpected behavior with the Docent platform, paus
8080
* If authentication fails (HTTP 401) or no API key is configured, walk the user through setup:
8181
1. Open the API keys page for them: `open https://docent.transluce.org/settings/api-keys` (macOS) or `xdg-open https://docent.transluce.org/settings/api-keys` (Linux).
8282
2. Ask them to create a new API key (it will start with `dk_`).
83-
3. Write the key to a local `docent.env` file or `~/.docent/docent.env`: `DOCENT_API_KEY=dk_...` (plus `DOCENT_API_URL` and `DOCENT_FRONTEND_URL` if not using the default instance).
83+
3. Write the key to `~/.docent/docent.env`: `DOCENT_API_KEY=dk_...` (plus `DOCENT_API_URL` and `DOCENT_FRONTEND_URL` if not using the default instance). Use a project-level `docent.env` only when the project needs a local override.
8484
4. Verify connectivity by constructing a `Docent()` client — the constructor validates the API key automatically.
8585
* If the SDK does not match what's documented here, check whether the SDK is up to date.
8686
* If the Docent MCP server is available but doesn't match the tools documented here, check whether the MCP server needs an upgrade (`uv tool upgrade docent`). If an upgrade was needed, ask the user to restart the session or MCP server.

plugins/docent/skills/docent/dql-reference.md

Lines changed: 148 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,8 @@ raw_rows = client.dql_result_to_dicts(result)
6969
| `name` | Optional transcript title. |
7070
| `description` | Optional description. |
7171
| `transcript_group_id` | Optional grouping identifier. |
72-
| `messages` | Binary-encoded JSON payload of message turns. |
73-
| `metadata_json` | Binary-encoded metadata describing the transcript. |
72+
| `messages` | UTF-8 bytes of a JSON array of message turns (Postgres `bytea`, not `jsonb`). Use `convert_from` before JSON operators or `jsonb_array_length` (see [Counting transcript messages](#counting-transcript-messages)). |
73+
| `metadata_json` | UTF-8 bytes of JSON metadata (`bytea`). Same `convert_from` pattern as `messages` when using JSON operators. |
7474
| `created_at` | Timestamp recorded during ingest. |
7575

7676
### `transcript_groups`
@@ -112,6 +112,8 @@ raw_rows = client.dql_result_to_dicts(result)
112112
| `dql_query` | DQL query (template readings only). |
113113
| `model_json` | Model configuration. |
114114
| `output_schema` | JSON schema for output validation. |
115+
| `max_new_tokens` | Maximum number of new tokens generated per LLM call. |
116+
| `num_rollouts` | Number of independent LLM samples generated per input row (>= 1). |
115117
| `source_reading_preset_id` | Optional associated preset. |
116118
| `created_at` | When the reading was created. |
117119

@@ -151,6 +153,7 @@ For scripted readings, `arguments_dict` holds arbitrary user-supplied metadata p
151153
| --- | --- |
152154
| `reading_id` | FK to readings.id. |
153155
| `result_id` | FK to reading_results.id. |
156+
| `rollout_index` | 0-based position of this rollout within the reading's group for the same input row. Range `[0, readings.num_rollouts)`. |
154157

155158
## JSON Metadata Access Patterns
156159

@@ -165,14 +168,20 @@ WHERE metadata_json->>'environment' = 'staging';
165168

166169
```sql
167170
-- Retrieve nested transcript metadata
171+
-- `transcripts.metadata_json` is bytea (UTF-8 JSON), not jsonb — decode before JSON operators.
168172
-- Dots in `get_metadata_fields` output (e.g. `metadata.conversation.speaker`) indicate nested JSON objects;
169173
-- traverse with -> for intermediate keys and ->> for the final key.
170174
SELECT
171175
id,
172-
metadata_json->'conversation'->>'speaker' AS speaker,
173-
metadata_json->'conversation'->>'topic' AS topic
174-
FROM transcripts
175-
WHERE metadata_json->>'status' = 'flagged';
176+
meta->'conversation'->>'speaker' AS speaker,
177+
meta->'conversation'->>'topic' AS topic
178+
FROM (
179+
SELECT
180+
id,
181+
convert_from(metadata_json, 'UTF8')::jsonb AS meta
182+
FROM transcripts
183+
) AS t
184+
WHERE meta->>'status' = 'flagged';
176185
```
177186

178187
```sql
@@ -185,6 +194,47 @@ WHERE metadata_json ? 'latency_ms';
185194

186195
When querying JSON fields, comparisons default to string semantics. Cast values when you need numeric ordering or aggregation.
187196

197+
## Counting transcript messages
198+
199+
`transcripts.messages` is stored as `bytea` (UTF-8 JSON), not `jsonb`. You cannot use `messages -> 0` or `jsonb_array_length(messages)` directly — Postgres reports `operator does not exist: bytea -> integer`.
200+
201+
Decode to `jsonb`, then count array elements:
202+
203+
```sql
204+
jsonb_array_length(convert_from(messages, 'UTF8')::jsonb)
205+
```
206+
207+
Allowed helpers: `convert_from`, `convert_to`, `jsonb_array_length`.
208+
209+
### Agent runs with at least N messages (any transcript)
210+
211+
```sql
212+
SELECT DISTINCT ar.id AS agent_run_id
213+
FROM agent_runs ar
214+
JOIN transcripts t ON t.agent_run_id = ar.id
215+
WHERE jsonb_array_length(convert_from(t.messages, 'UTF8')::jsonb) >= 10;
216+
```
217+
218+
### Per-transcript message counts
219+
220+
```sql
221+
SELECT
222+
transcript_id,
223+
agent_run_id,
224+
message_count
225+
FROM (
226+
SELECT
227+
t.id AS transcript_id,
228+
t.agent_run_id,
229+
jsonb_array_length(convert_from(t.messages, 'UTF8')::jsonb) AS message_count
230+
FROM transcripts t
231+
) AS counted
232+
WHERE message_count >= 10
233+
ORDER BY message_count DESC;
234+
```
235+
236+
Express filters like “≥10 messages” in DQL with the pattern above. Do not materialize matching run IDs elsewhere and paste them into a huge `WHERE id IN (...)` clause.
237+
188238
## Allowed Syntax
189239

190240
| Feature |
@@ -194,15 +244,15 @@ When querying JSON fields, comparisons default to string semantics. Cast values
194244
| `WITH` (CTEs) |
195245
| `UNION [ALL]`, `INTERSECT`, `EXCEPT` |
196246
| `GROUP BY`, `HAVING` |
197-
| Aggregations (`COUNT`, `AVG`, `MIN`, `MAX`, `SUM`, `STDDEV_POP`, `STDDEV_SAMP`, `VAR_POP`, `VAR_SAMP`, `ARRAY_AGG`, `STRING_AGG`, `JSON_AGG`, `JSONB_AGG`, `JSON_OBJECT_AGG`, `PERCENTILE_CONT`, `PERCENTILE_DISC` with `WITHIN GROUP`) |
247+
| Aggregations (`COUNT`, `AVG`, `MIN`, `MAX`, `SUM`, `STDDEV_POP`, `STDDEV_SAMP`, `VAR_POP`, `VAR_SAMP`, `ARRAY_AGG`, `STRING_AGG`, `JSON_AGG`, `JSONB_AGG`, `JSON_OBJECT_AGG`, `MODE`, `PERCENTILE_CONT`, `PERCENTILE_DISC` with `WITHIN GROUP`) |
198248
| Window functions (`ROW_NUMBER`, `RANK`, `DENSE_RANK`, `NTILE`, `LAG`, `LEAD`, `FIRST_VALUE`, `LAST_VALUE`, `NTH_VALUE`, `PERCENT_RANK`, `CUME_DIST`) |
199249
| `ORDER BY`, `LIMIT`, `OFFSET` |
200250
| Conditional & null helpers (`CASE`, `COALESCE`, `NULLIF`) |
201251
| Boolean logic (`AND`, `OR`, `NOT`) |
202252
| Comparison operators (`=`, `!=`, `<`, `<=`, `>`, `>=`, `IS`, `IS NOT`, `IS DISTINCT FROM`, `IN`, `BETWEEN`, `LIKE`, `ILIKE`, `EXISTS`, `SIMILAR TO`, `~`, `~*`, `!~`, `!~*`) |
203253
| Arithmetic & math (`+`, `-`, `*`, `/`, `%`, `POWER`, `ABS`, `SIGN`, `SQRT`, `LN`, `LOG`, `EXP`, `GREATEST`, `LEAST`, `FLOOR`, `CEIL`, `ROUND`, `RANDOM`) |
204254
| String helpers (`SUBSTRING`, `LEFT`, `RIGHT`, `LENGTH`, `UPPER`, `LOWER`, `INITCAP`, `TRIM`, `REPLACE`, `SPLIT_PART`, `POSITION`, `CONCAT`, `CONCAT_WS`, `STRING_AGG`) |
205-
| JSON operators & functions (`->`, `->>`, `#>`, `#>>`, `@>`, `?`, `?|`, `?&`, `jsonb_build_object`, `jsonb_build_array`, `json_agg`, `jsonb_agg`, `json_object_agg`, `jsonb_set`, `jsonb_path_query`, `jsonb_path_exists`) |
255+
| JSON operators & functions (`->`, `->>`, `#>`, `#>>`, `@>`, `?`, `?|`, `?&`, `jsonb_build_object`, `jsonb_build_array`, `jsonb_array_length`, `json_agg`, `jsonb_agg`, `json_object_agg`, `jsonb_set`, `jsonb_path_query`, `jsonb_path_exists`, `convert_from`, `convert_to`) |
206256
| Date/time basics (`CURRENT_DATE`, `CURRENT_TIME`, `CURRENT_TIMESTAMP`, `NOW()`, `EXTRACT`, `DATE_TRUNC`, `AGE`, `AT TIME ZONE`, `timezone()`) |
207257
| Interval arithmetic (`timestamp +/- INTERVAL`, `INTERVAL` literals, `MAKE_INTERVAL`, `JUSTIFY_DAYS`, `JUSTIFY_HOURS`, `JUSTIFY_INTERVAL`) |
208258
| Construction & conversion (`MAKE_DATE`, `MAKE_TIME`, `MAKE_TIMESTAMP`, `MAKE_TIMESTAMPTZ`, `TO_CHAR`, `TO_DATE`, `TO_TIMESTAMP`, `DATE_PART`) |
@@ -281,6 +331,92 @@ ORDER BY rr.id DESC
281331
LIMIT 50;
282332
```
283333

334+
### Rollouts and Self-Consistency
335+
336+
When a reading is configured with `num_rollouts > 1`, each input row produces multiple
337+
independent LLM samples. Rollouts are stored as separate `reading_results` rows joined
338+
to the reading via `reading_result_links`, with `reading_result_links.rollout_index`
339+
recording the 0-based position within the reading. Samples are fungible: a single
340+
result row may be linked by multiple readings (at potentially different rollout
341+
positions) when a cached sample is reused.
342+
343+
**Always filter out pending and failed rollouts before aggregating outputs.** The
344+
canonical predicate is:
345+
346+
```sql
347+
rr.output IS NOT NULL AND (rr.error IS NULL OR rr.error::text = 'null')
348+
```
349+
350+
`error` is JSONB, so SQL `NULL` and JSON `null` are both possible.
351+
352+
#### Per-row rollouts side by side
353+
354+
```sql
355+
SELECT
356+
rr.arguments_dict->'agent_run'->>'id' AS agent_run_id,
357+
rrl.rollout_index,
358+
rr.output->>'answer' AS answer
359+
FROM reading_results rr
360+
JOIN reading_result_links rrl ON rrl.result_id = rr.id
361+
WHERE rrl.reading_id = '<reading-uuid>'
362+
AND rr.output IS NOT NULL
363+
ORDER BY agent_run_id, rrl.rollout_index;
364+
```
365+
366+
#### Per-row self-consistency and modal vote
367+
368+
`COUNT(DISTINCT ...)` measures spread; `MODE() WITHIN GROUP` picks the majority answer.
369+
370+
```sql
371+
SELECT
372+
rr.arguments_dict->'agent_run'->>'id' AS agent_run_id,
373+
COUNT(rr.id) AS n_completed,
374+
COUNT(DISTINCT rr.output->>'answer') AS n_distinct_answers,
375+
MODE() WITHIN GROUP (ORDER BY rr.output->>'answer') AS modal_answer
376+
FROM reading_results rr
377+
JOIN reading_result_links rrl ON rrl.result_id = rr.id
378+
WHERE rrl.reading_id = '<reading-uuid>'
379+
AND rr.output IS NOT NULL
380+
GROUP BY agent_run_id;
381+
```
382+
383+
To compare two readings, wrap this query (selecting `agent_run_id, modal_answer`) as a
384+
subquery per reading and join the two on `agent_run_id`.
385+
386+
#### Reading-level self-consistency rate
387+
388+
```sql
389+
SELECT
390+
reading_id,
391+
ROUND(CAST(AVG(CASE WHEN n_distinct = 1 THEN 1.0 ELSE 0.0 END) AS NUMERIC), 3)
392+
AS unanimous_row_fraction,
393+
ROUND(CAST(AVG(n_distinct) AS NUMERIC), 3) AS avg_distinct_per_row
394+
FROM (
395+
SELECT
396+
rrl.reading_id AS reading_id,
397+
rr.cache_key_hash AS row_key,
398+
COUNT(DISTINCT rr.output->>'answer') AS n_distinct
399+
FROM reading_results rr
400+
JOIN reading_result_links rrl ON rrl.result_id = rr.id
401+
WHERE rrl.reading_id IN ('<reading-A>', '<reading-B>')
402+
AND rr.output IS NOT NULL
403+
GROUP BY rrl.reading_id, rr.cache_key_hash
404+
) AS row_stats
405+
GROUP BY reading_id;
406+
```
407+
408+
**Counting semantics.** Because cached samples are pooled across readings, a single
409+
`reading_results` row may appear in multiple readings via different links. Choose:
410+
411+
- `COUNT(rr.id)` or `COUNT(rrl.result_id)` — counts links (i.e. rollouts as seen by
412+
this reading set). What you usually want.
413+
- `COUNT(DISTINCT rr.id)` — counts unique LLM calls (i.e. the underlying sample pool).
414+
415+
**Rollout pairing caveat.** `rollout_index` is per-link, not per-result: rollout #2 of
416+
reading A and rollout #2 of reading B are not paired draws. Avoid joining across
417+
readings on `(input, rollout_index)` for paired tests — fungible samples have no
418+
positional identity across readings.
419+
284420
## Restrictions and Best Practices
285421

286422
- **Read-only**: Only `SELECT`-style queries are permitted.
@@ -290,6 +426,7 @@ LIMIT 50;
290426
- **Limit enforcement**: Every query is capped at 10,000 rows. Use pagination (`OFFSET`/`LIMIT`) for larger row collections.
291427
- **JSON performance**: Heavy JSON traversal across large collections can be slow. Prefer top-level fields when available.
292428
- **Type awareness**: Cast values explicitly when precision matters.
429+
- **Reading results: filter by completion.** Querying `reading_results` will include pending and failed rollouts by default. Add `WHERE rr.output IS NOT NULL AND (rr.error IS NULL OR rr.error::text = 'null')` to any aggregation that should ignore them.
293430

294431
## DQL quirks
295432

@@ -339,6 +476,9 @@ FROM (...) AS subq
339476
GROUP BY task
340477
```
341478

479+
### Do not inline large lists of precomputed IDs
480+
When a filter depends on transcript shape (message count, metadata, joins), compute it in DQL with `JOIN`/`WHERE`/`GROUP BY` — not by pasting hundreds of UUIDs into `WHERE agent_runs.id IN ('…', '…', …)`. That pattern is hard to maintain, blows query size limits, and usually means the real filter belongs in SQL (see [Counting transcript messages](#counting-transcript-messages)).
481+
342482
### Avoid Dynamic IN Clauses with String Interpolation
343483
Building IN clauses with f-strings is dangerous:
344484
- Task names containing `::` can be parsed as PostgreSQL type casts

plugins/docent/skills/docent/ingestion.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ Before Python work, use an existing virtual environment if present. If no enviro
3636

3737
Collect only what is needed to plan:
3838

39-
- API key: prefer `$DOCENT_API_KEY` or an SDK-discovered `docent.env` (current directory upward, then `~/.docent/docent.env`); ask only if neither is available.
39+
- API key: prefer `$DOCENT_API_KEY` or an SDK-discovered config file. The default global file is `~/.docent/docent.env`; project-level `docent.env` files are supported as local overrides.
4040
- Data path: the file or directory to ingest.
4141
- Optional context: what produced the data and what analysis the user wants to do in Docent.
4242

plugins/docent/skills/docent/readings-reference.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,17 @@ Glob filter rules:
298298
* Common pitfall: do not set `transcript_group_names=GlobFilter(include=("*",))` when the user asks to render only a specific transcript name. Including all transcript groups makes all visible descendants render, so it can override the intended narrow transcript selection. In that case, make `transcript_group_names` exclude-all and set only `transcript_names=GlobFilter(include=("<requested transcript name>",))`.
299299
* Transcript group filtering is path-scoped. Including a nested group makes that group and its visible descendants render, and any ancestors needed to reach it may render as wrappers. It does not make sibling branches visible. For example, if `G1` contains both `G2 -> G3` and `G2-prime`, including `G3` can render wrapper groups `G1` and `G2`, but `G2-prime` remains hidden unless it or one of its descendants is independently included.
300300

301+
### Multiple rollouts
302+
303+
If the user asks for multiple rollouts, you can use the `num_rollouts` parameter. Leave it unset (defaults to 1) unless the user explicitly asks.
304+
```python
305+
reading = client.read(
306+
prompt_template=["Summarize: ", rows.transcript.as_type("transcript")],
307+
model="openai/gpt-5.4-mini",
308+
num_rollouts=3
309+
)
310+
```
311+
301312
### `client.step_group(label) -> StepGroupContext`
302313
Opens a labeled step group in the session UI. Use as a context manager to auto-close the group scope:
303314
```python

0 commit comments

Comments
 (0)