Run prettier on backlog folder
This commit is contained in:
@@ -3,8 +3,8 @@ id: ML-144
|
||||
title: Implement magazine issue ingestion pipeline
|
||||
status: To Do
|
||||
assignee: []
|
||||
created_date: '2026-04-22 15:38'
|
||||
updated_date: '2026-05-04 08:19'
|
||||
created_date: "2026-04-22 15:38"
|
||||
updated_date: "2026-05-04 08:19"
|
||||
labels: []
|
||||
dependencies: []
|
||||
priority: medium
|
||||
@@ -13,16 +13,20 @@ priority: medium
|
||||
## Description
|
||||
|
||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||
|
||||
Build a pipeline to ingest music magazine issues (PDF), extract structured metadata (artist mentions, album reviews, upcoming releases, features), and surface relevant information in the app.
|
||||
|
||||
## Approach (Approved)
|
||||
|
||||
**Approach 1 — Text-first, single LLM pass with structured output:**
|
||||
|
||||
- Extract text locally with pdftotext (poppler, shell from Elixir) or [kreuzberg](https://hex.pm/packages/kreuzberg)
|
||||
- Feed chunks to OpenAI Responses API with JSON schema for structured output
|
||||
- One Oban worker per issue, one API call per chunk
|
||||
- Cost estimate: ~$0.01–0.10 per issue with gpt-4o-mini
|
||||
|
||||
## Pipeline Overview
|
||||
|
||||
1. Upload PDF → store as Asset
|
||||
2. Enqueue `IngestIssue` worker (new `magazines` queue, concurrency 1)
|
||||
3. Extract text locally (pdftotext)
|
||||
@@ -34,6 +38,7 @@ Build a pipeline to ingest music magazine issues (PDF), extract structured metad
|
||||
7. Resolve each mention against existing records/artists by name (MusicBrainz lookup fallback for unknowns)
|
||||
|
||||
## Personalization Layer
|
||||
|
||||
- Load collection's artist MBIDs once per issue
|
||||
- Match mentions by name → MBID (exact match, then fuzzy / MusicBrainz search)
|
||||
- For matched artists/albums: create Note linked via musicbrainz_id with article snippet
|
||||
@@ -41,6 +46,7 @@ Build a pipeline to ingest music magazine issues (PDF), extract structured metad
|
||||
- Upcoming releases: new schema or flag on notes with reminder job
|
||||
|
||||
## Integration Points
|
||||
|
||||
- New context: `MusicLibrary.Magazines` with `Issue` and `Mention` schemas
|
||||
- New Oban queue: `magazines` (concurrency 1)
|
||||
- Workers: `IngestIssue`, `ExtractArticle`, `ResolveMentions`
|
||||
@@ -49,7 +55,9 @@ Build a pipeline to ingest music magazine issues (PDF), extract structured metad
|
||||
<!-- SECTION:DESCRIPTION:END -->
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
<!-- AC:BEGIN -->
|
||||
|
||||
- [ ] #1 Pipeline can ingest a magazine PDF and extract text without OCR
|
||||
- [ ] #2 Structured extraction (artist/album mentions, topics) is persisted to database
|
||||
- [ ] #3 Mentions are resolved against existing collection with MusicBrainz fallback lookup
|
||||
@@ -59,7 +67,9 @@ Build a pipeline to ingest music magazine issues (PDF), extract structured metad
|
||||
<!-- AC:END -->
|
||||
|
||||
## Definition of Done
|
||||
|
||||
<!-- DOD:BEGIN -->
|
||||
|
||||
- [ ] #1 I can open a page in the application, drag and drop a magazine PDF, and start ingestion
|
||||
- [ ] #2 For each artist/album in the collection and Wishlist, I can see relevant informational articles coming from ingested magazines
|
||||
- [ ] #3 I can see a list of ALL ingested items, even those which don't have direct correlation with existing records and artists
|
||||
|
||||
+22
-4
@@ -5,13 +5,13 @@ title: >-
|
||||
to LLM for collection chat
|
||||
status: To Do
|
||||
assignee: []
|
||||
created_date: '2026-05-02 16:02'
|
||||
updated_date: '2026-05-04 08:19'
|
||||
created_date: "2026-05-02 16:02"
|
||||
updated_date: "2026-05-04 08:19"
|
||||
labels:
|
||||
- ready
|
||||
dependencies: []
|
||||
references:
|
||||
- 'backlog://document/doc-1'
|
||||
- "backlog://document/doc-1"
|
||||
documentation:
|
||||
- lib/music_library/chats/collection_chat.ex
|
||||
- lib/music_library/collection.ex
|
||||
@@ -26,18 +26,23 @@ priority: high
|
||||
## Description
|
||||
|
||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||
|
||||
Currently, every new collection chat sends the ENTIRE collection catalog (all records formatted as "Artist - Title (year, format) [genres]") plus aggregated stats as the `instructions` parameter to the OpenAI Responses API. For a collection of 500+ records, this burns ~9,000+ input tokens on EVERY new chat start — regardless of what the user asks.
|
||||
|
||||
The goal of this task is to analyze alternatives, pick the best one, and implement it. The selected approach should:
|
||||
|
||||
- Significantly reduce per-chat token usage
|
||||
- Preserve or improve the quality of LLM responses about the collection
|
||||
- Not require architectural overhauls beyond the chat/streaming layer
|
||||
|
||||
The `Collection.collection_summary/0` function loads ALL records from the DB, formats them, and returns `{summary, count}`. This is computed asynchronously in `CollectionLive.Index.mount/3` and passed to the Chat component as `chat_context`. `CollectionChat.build_instructions/2` then embeds the full summary into the instructions string sent to OpenAI.
|
||||
|
||||
<!-- SECTION:DESCRIPTION:END -->
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
<!-- AC:BEGIN -->
|
||||
|
||||
- [ ] #1 `CollectionChat.build_instructions/2` no longer interpolates the full collection catalog into the instructions sent to OpenAI — only aggregated stats and a record count are included
|
||||
- [ ] #2 A `file_search` tool with the collection's vector store ID is included in every collection chat request to the OpenAI Responses API
|
||||
- [ ] #3 A `CollectionChat.FileStore` module manages the collection file lifecycle: upload to OpenAI Files API, vector store creation, and file-to-store attachment, persisting IDs via `Secrets`
|
||||
@@ -51,6 +56,7 @@ The `Collection.collection_summary/0` function loads ALL records from the DB, fo
|
||||
## Implementation Plan
|
||||
|
||||
<!-- SECTION:PLAN:BEGIN -->
|
||||
|
||||
## Approach: OpenAI `file_search` tool + Oban-managed file lifecycle
|
||||
|
||||
Upload the collection catalog as a file to OpenAI, create a vector store, and use the Responses API's built-in `file_search` tool. OpenAI automatically performs semantic search over the file and includes relevant results inline in the response stream — no SSE event handling changes, no orchestration loop.
|
||||
@@ -93,7 +99,7 @@ defmodule MusicLibrary.Chats.CollectionChat.FileStore do
|
||||
@moduledoc """
|
||||
Manages the collection catalog file lifecycle at OpenAI.
|
||||
Persists file_id and vector_store_id via Secrets.
|
||||
|
||||
|
||||
All OpenAI API calls happen inside Oban workers — never from chat
|
||||
sessions or LiveViews. The chat session path only calls get_vector_store_id/0.
|
||||
"""
|
||||
@@ -139,6 +145,7 @@ end
|
||||
```
|
||||
|
||||
Key design properties:
|
||||
|
||||
- `upload_or_refresh/0` is the single entry point called by the Oban worker — never by chat sessions
|
||||
- `get_vector_store_id/0` is the single entry point called by chat sessions — read-only, no side effects
|
||||
- `cleanup_orphaned_files/0` is a safety net for edge cases (e.g., Secrets write failed after OpenAI upload succeeded)
|
||||
@@ -175,6 +182,7 @@ end
|
||||
- Queue `:default` (concurrency 10) — the worker is fast (a few API calls), no need for a dedicated queue.
|
||||
|
||||
Enqueued from two places:
|
||||
|
||||
1. **Record mutations** — `Records.create_record/1`, `update_record/2`, `delete_record/1` (see Phase 4)
|
||||
2. **Cron** — every hour in both dev and prod, to handle initial upload after deploy and recovery after failures:
|
||||
|
||||
@@ -414,6 +422,7 @@ end
|
||||
```
|
||||
|
||||
Key changes from current code:
|
||||
|
||||
- `stream_response/3` keeps the `{tuple, count}` signature shape — the stats string replaces the catalog text
|
||||
- `build_instructions/2` interpolates aggregated stats (~200 tokens) instead of the full catalog (~9,000 tokens)
|
||||
- `FileStore.get_vector_store_id/0` is the only FileStore function called from the chat path
|
||||
@@ -430,6 +439,7 @@ Key changes from current code:
|
||||
#### 6a. OpenAI.API endpoint tests (`test/open_ai/api_test.exs`)
|
||||
|
||||
Add test blocks for the 7 new endpoints, following existing `Req.Test.stub` patterns:
|
||||
|
||||
- `upload_file/3` — verify multipart body includes `purpose: "assistants"` and file content; test success (200 + file JSON) and error (500) responses
|
||||
- `create_vector_store/2` — verify request body has `name` field; test success and error
|
||||
- `add_file_to_vector_store/3` — verify URL path includes store_id; test success and error
|
||||
@@ -444,6 +454,7 @@ Add test blocks for the 7 new endpoints, following existing `Req.Test.stub` patt
|
||||
#### 6b. FileStore tests (`test/music_library/chats/collection_chat/file_store_test.exs`)
|
||||
|
||||
Test `FileStore` functions with `Req.Test` stubs and direct `Secrets` manipulation:
|
||||
|
||||
- `upload_or_refresh/0` — first-time upload (no secrets → creates file + store + attaches → secrets populated)
|
||||
- `upload_or_refresh/0` — refresh (secrets present → uploads new file → attaches → detaches old → deletes old → updates file_id in secrets)
|
||||
- `upload_or_refresh/0` — handles file still "uploaded" (polls until "processed")
|
||||
@@ -457,6 +468,7 @@ Test `FileStore` functions with `Req.Test` stubs and direct `Secrets` manipulati
|
||||
#### 6c. CollectionChat tests (`test/music_library/chats/collection_chat_test.exs`)
|
||||
|
||||
Update existing tests and add new ones:
|
||||
|
||||
- Verify instructions **no longer** contain the per-record catalog lines (the key regression test)
|
||||
- Verify instructions **do** contain the aggregated stats string and record count
|
||||
- Verify `file_search` tool is included in the API request body when vector store is available (stub `FileStore.get_vector_store_id/0` → `{:ok, "vs_test"}` and assert `tools` array in the JSON body contains `%{"type" => "file_search", "vector_store_ids" => ["vs_test"]}`)
|
||||
@@ -478,6 +490,7 @@ Update existing tests and add new ones:
|
||||
#### 6e. Records context integration tests
|
||||
|
||||
In existing `test/music_library/records_test.exs` (or equivalent):
|
||||
|
||||
- `create_record/1` enqueues `RefreshCollectionFile` worker
|
||||
- `update_record/2` enqueues `RefreshCollectionFile` worker
|
||||
- `delete_record/1` enqueues `RefreshCollectionFile` worker
|
||||
@@ -488,6 +501,7 @@ In existing `test/music_library/records_test.exs` (or equivalent):
|
||||
#### 6f. LiveView test
|
||||
|
||||
In `test/music_library_web/live/collection_live/index_test.exs` (or equivalent):
|
||||
|
||||
- Verify `@collection_stats` is set after mount (not `@collection_summary`)
|
||||
- Verify Chat component receives `{stats_string, count}` tuple as `chat_context`
|
||||
|
||||
@@ -496,6 +510,7 @@ In `test/music_library_web/live/collection_live/index_test.exs` (or equivalent):
|
||||
#### 6g. Collection context tests
|
||||
|
||||
In existing `test/music_library/collection_test.exs` (or equivalent):
|
||||
|
||||
- `stats_summary/0` returns stats string with genre/format/era breakdowns and correct group count
|
||||
- `stats_summary/0` returns `{"", 0}` for empty collection
|
||||
- `count_records/0` returns correct count
|
||||
@@ -504,10 +519,13 @@ In existing `test/music_library/collection_test.exs` (or equivalent):
|
||||
~20 lines.
|
||||
|
||||
Estimated total: ~510 lines across 13-15 files. Risk: low/medium — the core streaming infrastructure (`decode_responses_event/2`, Chat component `do_send_message/2`, SSE event loop) is untouched. New code is additive (API endpoints, Oban worker, FileStore module) and follows existing patterns.
|
||||
|
||||
<!-- SECTION:PLAN:END -->
|
||||
|
||||
## Definition of Done
|
||||
|
||||
<!-- DOD:BEGIN -->
|
||||
|
||||
- [ ] #1 All new and modified modules have @moduledoc
|
||||
- [ ] #2 All public functions have @spec and @doc
|
||||
- [ ] #3 Mix compile --warnings-as-errors passes
|
||||
|
||||
@@ -3,23 +3,27 @@ id: ML-160
|
||||
title: Programmatic access to production logs
|
||||
status: Done
|
||||
assignee: []
|
||||
created_date: '2026-05-04 06:42'
|
||||
updated_date: '2026-05-04 07:33'
|
||||
created_date: "2026-05-04 06:42"
|
||||
updated_date: "2026-05-04 07:33"
|
||||
labels: []
|
||||
dependencies: []
|
||||
references:
|
||||
- 'backlog://document/doc-6'
|
||||
- "backlog://document/doc-6"
|
||||
priority: medium
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||
|
||||
Evaluate and implement the best approach for the LLM to access production logs. The /prod-logs pi extension lets the user see production logs via an interactive TUI, but the LLM has no access beyond the user selecting lines and pasting. The goal is to produce an extension/skill/tool that the LLM can use automatically when it needs to pull production logs. Reading logs from production is already implemented in /prod-logs as a `fetchLogs` function that hits the Coolify API — that code should be reused/shared where possible.
|
||||
|
||||
<!-- SECTION:DESCRIPTION:END -->
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
<!-- AC:BEGIN -->
|
||||
|
||||
- [x] #1 The LLM can fetch production logs without user intervention by calling the fetch_production_logs tool
|
||||
- [x] #2 The tool supports a `tail` parameter to limit the number of log lines returned (default: 200)
|
||||
- [x] #3 The tool supports a `grep` parameter for case-insensitive filtering of log lines
|
||||
@@ -32,6 +36,7 @@ Evaluate and implement the best approach for the LLM to access production logs.
|
||||
## Implementation Plan
|
||||
|
||||
<!-- SECTION:PLAN:BEGIN -->
|
||||
|
||||
# Implementation Plan
|
||||
|
||||
## Route: Pi Tool via `pi.registerTool()` (Route B)
|
||||
@@ -45,11 +50,13 @@ Add a `fetch_production_logs` tool to the existing `prod-logs` pi extension usin
|
||||
**File**: `.pi/extensions/prod-logs/index.ts`
|
||||
|
||||
**Changes**:
|
||||
|
||||
1. Add `import { Type } from "typebox";` to the existing imports block
|
||||
2. Add `import { truncateTail, formatSize, DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES } from "@mariozechner/pi-coding-agent";` to the imports block
|
||||
3. Inside the `prodLogsExtension()` default export function, add a `pi.registerTool()` call (before or after the existing `pi.registerCommand()` call)
|
||||
|
||||
**Tool specification**:
|
||||
|
||||
- `name`: `"fetch_production_logs"`
|
||||
- `description`: Explains when the LLM should use this tool (investigating production errors, checking server behavior, debugging deployed issues). Mentions the 50KB/2000-line truncation limit.
|
||||
- `promptSnippet`: `"Fetch recent production logs from Coolify (param: tail, grep)"`
|
||||
@@ -59,6 +66,7 @@ Add a `fetch_production_logs` tool to the existing `prod-logs` pi extension usin
|
||||
- `grep` — `Type.Optional(Type.String())`, case-insensitive filter pattern
|
||||
|
||||
**Execute handler logic**:
|
||||
|
||||
1. Check `signal?.aborted` early — if aborted, return `{ content: [{ type: "text", text: "Cancelled" }] }`
|
||||
2. Read Coolify credentials via `resolveVar("coolify_host")`, `resolveVar("coolify_app_uuid")`, `resolveVar("coolify_token")`
|
||||
3. If any credential is missing, return an error message listing which env vars are missing
|
||||
@@ -73,6 +81,7 @@ Add a `fetch_production_logs` tool to the existing `prod-logs` pi extension usin
|
||||
12. Return as text content, with `details: { lineCount }` (the pre-truncation line count)
|
||||
|
||||
### Verification
|
||||
|
||||
1. Run `/reload` in pi to hot-reload the extension
|
||||
2. Ask the LLM: "What tools are available for fetching production logs?" — it should describe `fetch_production_logs`
|
||||
3. Ask the LLM: "Fetch the last 50 lines of production logs" — verify it calls the tool and returns log text
|
||||
@@ -85,27 +94,27 @@ Add a `fetch_production_logs` tool to the existing `prod-logs` pi extension usin
|
||||
|
||||
## Architecture Impact Analysis
|
||||
|
||||
| Touchpoint | Impact |
|
||||
|------------|--------|
|
||||
| `.pi/extensions/prod-logs/index.ts` | **Modified** — ~70 lines added (imports + tool registration + truncation) |
|
||||
| Elixir modules | **None** — no changes |
|
||||
| Schemas, PubSub, routes, UI | **None** — pi-only change |
|
||||
| Config / env vars | **None** — same `HURL_VARIABLE_coolify_*` vars already in use |
|
||||
| Existing `/prod-logs` command | **Unchanged** — tool registration is additive, independent of command registration |
|
||||
| Touchpoint | Impact |
|
||||
| ----------------------------------- | ---------------------------------------------------------------------------------- |
|
||||
| `.pi/extensions/prod-logs/index.ts` | **Modified** — ~70 lines added (imports + tool registration + truncation) |
|
||||
| Elixir modules | **None** — no changes |
|
||||
| Schemas, PubSub, routes, UI | **None** — pi-only change |
|
||||
| Config / env vars | **None** — same `HURL_VARIABLE_coolify_*` vars already in use |
|
||||
| Existing `/prod-logs` command | **Unchanged** — tool registration is additive, independent of command registration |
|
||||
|
||||
---
|
||||
|
||||
## Performance Profile
|
||||
|
||||
| Aspect | Characteristic |
|
||||
|--------|---------------|
|
||||
| **Runtime complexity** | O(n) for grep filtering (single pass over log lines), O(1) for tail slice, O(n) for truncation byte counting |
|
||||
| **Network** | Single HTTP GET to Coolify API; no retries in tool handler |
|
||||
| **Memory** | Log response held in memory as string array; typically < 1MB for a few thousand lines; truncation caps return value at 50KB |
|
||||
| **Latency** | Dominated by Coolify API response time (typically 1-5 seconds); local filtering negligible |
|
||||
| **Database** | No database queries |
|
||||
| **State** | Stateless — no caching, no persistence |
|
||||
| **N+1 risk** | None — single API call |
|
||||
| Aspect | Characteristic |
|
||||
| ---------------------- | --------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Runtime complexity** | O(n) for grep filtering (single pass over log lines), O(1) for tail slice, O(n) for truncation byte counting |
|
||||
| **Network** | Single HTTP GET to Coolify API; no retries in tool handler |
|
||||
| **Memory** | Log response held in memory as string array; typically < 1MB for a few thousand lines; truncation caps return value at 50KB |
|
||||
| **Latency** | Dominated by Coolify API response time (typically 1-5 seconds); local filtering negligible |
|
||||
| **Database** | No database queries |
|
||||
| **State** | Stateless — no caching, no persistence |
|
||||
| **N+1 risk** | None — single API call |
|
||||
|
||||
---
|
||||
|
||||
@@ -118,6 +127,7 @@ No paid resources consumed. The Coolify API is self-hosted as part of the deploy
|
||||
## Production Infrastructure Steps
|
||||
|
||||
No production changes required:
|
||||
|
||||
- The `HURL_VARIABLE_coolify_*` environment variables are already configured in the pi runtime environment (the existing `/prod-logs` command already depends on them)
|
||||
- No new environment variables, service provisioning, DNS changes, or firewall rules
|
||||
- No database migrations
|
||||
@@ -128,6 +138,7 @@ No production changes required:
|
||||
## Documentation Updates
|
||||
|
||||
No project documentation files need updating:
|
||||
|
||||
- `docs/architecture.md` — already covers pi extensions generically; no new Elixir modules or architectural patterns introduced
|
||||
- `docs/project-conventions.md` — no new conventions introduced
|
||||
- `docs/production-infrastructure.md` — no infrastructure changes
|
||||
@@ -147,6 +158,7 @@ The implementation is self-documenting: the tool's `description`, `promptSnippet
|
||||
## Implementation Notes
|
||||
|
||||
<!-- SECTION:NOTES:BEGIN -->
|
||||
|
||||
Added `fetch_production_logs` tool to the existing `.pi/extensions/prod-logs/index.ts` extension. The tool reuses the existing `fetchLogs()` and `resolveVar()` helper functions. Implementation follows the plan exactly:
|
||||
|
||||
- Added imports for `Type` (typebox) and `truncateTail`, `formatSize`, `DEFAULT_MAX_BYTES`, `DEFAULT_MAX_LINES` (@mariozechner/pi-coding-agent)
|
||||
@@ -160,9 +172,11 @@ Added `fetch_production_logs` tool to the existing `.pi/extensions/prod-logs/ind
|
||||
## Final Summary
|
||||
|
||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||
|
||||
Added a `fetch_production_logs` tool to `.pi/extensions/prod-logs/index.ts` using `pi.registerTool()`. The tool gives the LLM programmatic access to production logs without user intervention.
|
||||
|
||||
**What changed:**
|
||||
|
||||
- Added imports for `Type` (typebox) and `truncateTail`, `formatSize`, `DEFAULT_MAX_BYTES`, `DEFAULT_MAX_LINES` from `@mariozechner/pi-coding-agent`
|
||||
- Registered `fetch_production_logs` tool with `tail` (default 200) and `grep` parameters
|
||||
- Tool reuses the existing `fetchLogs()` and `resolveVar()` module-scope functions from the `/prod-logs` extension
|
||||
@@ -170,10 +184,12 @@ Added a `fetch_production_logs` tool to `.pi/extensions/prod-logs/index.ts` usin
|
||||
- On missing credentials, returns a clear error listing which env vars are not set
|
||||
|
||||
**What didn't change:**
|
||||
|
||||
- The existing `/prod-logs` interactive command code is completely untouched
|
||||
- No new dependencies, env vars, infrastructure changes, or Elixir module changes
|
||||
|
||||
**Verification (requires `/reload` in pi):**
|
||||
|
||||
1. Ask the LLM about available tools for production logs — should describe `fetch_production_logs`
|
||||
2. Ask the LLM to fetch logs with `tail: 50` or `grep: "error"` — should work
|
||||
3. Unset a credential and try — should get clear error message
|
||||
|
||||
@@ -3,8 +3,8 @@ id: ML-161
|
||||
title: pi access to production errors
|
||||
status: Done
|
||||
assignee: []
|
||||
created_date: '2026-05-04 08:06'
|
||||
updated_date: '2026-05-04 13:18'
|
||||
created_date: "2026-05-04 08:06"
|
||||
updated_date: "2026-05-04 13:18"
|
||||
labels:
|
||||
- pi
|
||||
- api
|
||||
@@ -13,7 +13,7 @@ dependencies:
|
||||
- ML-163
|
||||
- ML-164
|
||||
references:
|
||||
- 'backlog://document/doc-7'
|
||||
- "backlog://document/doc-7"
|
||||
priority: medium
|
||||
ordinal: 9000
|
||||
---
|
||||
@@ -21,6 +21,7 @@ ordinal: 9000
|
||||
## Description
|
||||
|
||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||
|
||||
Errors in production are captured via the `error_tracker` Elixir dependency and accessed via a dedicated dashboard at `/dev/errors`. There is no built-in tooling or endpoint to pull production errors programmatically in a pi session. This task covers the investigation and implementation of the best approach to expose production error data to pi, broken into three subtasks:
|
||||
|
||||
1. **Expose production errors via a programmatic API (behind auth)** — Add a JSON API endpoint under `/api/v1/errors` that lists errors and shows individual error details from the error_tracker tables.
|
||||
@@ -32,6 +33,7 @@ Errors in production are captured via the `error_tracker` Elixir dependency and
|
||||
These are the attributes that subtask implementations must treat as the canonical data model:
|
||||
|
||||
**Error (from `error_tracker_errors` table):**
|
||||
|
||||
- `id` — UUID, unique error identifier
|
||||
- `kind` — string, e.g. "error", "throw", "exit"
|
||||
- `reason` — string, the error message/reason
|
||||
@@ -44,6 +46,7 @@ These are the attributes that subtask implementations must treat as the canonica
|
||||
- `inserted_at` / `updated_at` — UTC datetime
|
||||
|
||||
**Occurrence (from `error_tracker_occurrences` table):**
|
||||
|
||||
- `id` — UUID
|
||||
- `reason` — string, error reason at time of occurrence
|
||||
- `context` — map, includes `live_view.view`, `request.path`, etc.
|
||||
@@ -53,6 +56,7 @@ These are the attributes that subtask implementations must treat as the canonica
|
||||
- `inserted_at` — UTC datetime
|
||||
|
||||
**Aggregated/per-error metadata (computed, not stored):**
|
||||
|
||||
- `occurrence_count` — total occurrences for this error
|
||||
- `first_occurrence_at` — earliest occurrence timestamp
|
||||
<!-- SECTION:DESCRIPTION:END -->
|
||||
|
||||
@@ -3,8 +3,8 @@ id: ML-162
|
||||
title: Expose production errors via JSON API endpoint
|
||||
status: Done
|
||||
assignee: []
|
||||
created_date: '2026-05-04 08:08'
|
||||
updated_date: '2026-05-04 12:11'
|
||||
created_date: "2026-05-04 08:08"
|
||||
updated_date: "2026-05-04 12:11"
|
||||
labels:
|
||||
- api
|
||||
- ready
|
||||
@@ -17,6 +17,7 @@ ordinal: 7000
|
||||
## Description
|
||||
|
||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||
|
||||
Add an API controller and routes under `/api/v1/errors` to expose ErrorTracker data as JSON, behind the existing Bearer token auth.
|
||||
|
||||
This subtask covers the server-side work only: controller, JSON serialization, context queries, and routes. The pi tooling and extensions are covered by separate subtasks.
|
||||
@@ -24,11 +25,13 @@ This subtask covers the server-side work only: controller, JSON serialization, c
|
||||
### API design
|
||||
|
||||
**`GET /api/v1/errors`** — List errors
|
||||
|
||||
- Query params: `status` (resolved/unresolved), `muted` (true/false), `search` (substring match on reason), `limit` (default 50), `offset` (default 0)
|
||||
- Returns: `{ errors: [...], total: n, limit: n, offset: n }`
|
||||
- Each error includes: id, kind, reason, source_line, source_function, status, fingerprint, last_occurrence_at, muted, inserted_at, updated_at, occurrence_count, first_occurrence_at
|
||||
|
||||
**`GET /api/v1/errors/:id`** — Single error detail
|
||||
|
||||
- Returns the error with all its occurrences (with stacktraces), sorted by inserted_at desc
|
||||
- Each occurrence includes: id, reason, context, breadcrumbs, stacktrace (lines), inserted_at
|
||||
|
||||
@@ -48,6 +51,7 @@ This subtask covers the server-side work only: controller, JSON serialization, c
|
||||
## Implementation Plan
|
||||
|
||||
<!-- SECTION:PLAN:BEGIN -->
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Objective alignment
|
||||
@@ -72,27 +76,29 @@ Add two JSON API endpoints under `/api/v1/errors` (already behind the existing B
|
||||
|
||||
### Architecture impact analysis
|
||||
|
||||
| Touchpoint | Impact |
|
||||
|---|---|
|
||||
| **New context: `lib/music_library/errors.ex`** | New module. Two public functions: `list_errors/1` (filtered + paginated list with counts) and `get_error!/1` (single error with occurrences). |
|
||||
| **New controller: `lib/music_library_web/controllers/error_controller.ex`** | New module. Two actions: `index/2`, `show/2`. Parses query params with `parse_int` helper. |
|
||||
| **New JSON view: `lib/music_library_web/controllers/error_json.ex`** | New module. Two render functions: `index/1`, `show/1`. Serializes errors and occurrences. |
|
||||
| **Router: `lib/music_library_web/router.ex`** | Add two new routes under the existing `scope "/api/v1"` block (`GET /api/v1/errors`, `GET /api/v1/errors/:id`). No new pipeline or scope. |
|
||||
| **No PubSub impact** | These are read-only endpoints. No real-time updates needed. |
|
||||
| **No supervision tree impact** | The context is stateless. No new processes. |
|
||||
| **No external API impact** | Internal API only. |
|
||||
| **No migration needed** | Tables already exist (created by error_tracker's own migrations in `20260226212444_add_error_tracker.exs`). |
|
||||
| **No deprecation path** | Net-new addition. |
|
||||
| Touchpoint | Impact |
|
||||
| --------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **New context: `lib/music_library/errors.ex`** | New module. Two public functions: `list_errors/1` (filtered + paginated list with counts) and `get_error!/1` (single error with occurrences). |
|
||||
| **New controller: `lib/music_library_web/controllers/error_controller.ex`** | New module. Two actions: `index/2`, `show/2`. Parses query params with `parse_int` helper. |
|
||||
| **New JSON view: `lib/music_library_web/controllers/error_json.ex`** | New module. Two render functions: `index/1`, `show/1`. Serializes errors and occurrences. |
|
||||
| **Router: `lib/music_library_web/router.ex`** | Add two new routes under the existing `scope "/api/v1"` block (`GET /api/v1/errors`, `GET /api/v1/errors/:id`). No new pipeline or scope. |
|
||||
| **No PubSub impact** | These are read-only endpoints. No real-time updates needed. |
|
||||
| **No supervision tree impact** | The context is stateless. No new processes. |
|
||||
| **No external API impact** | Internal API only. |
|
||||
| **No migration needed** | Tables already exist (created by error_tracker's own migrations in `20260226212444_add_error_tracker.exs`). |
|
||||
| **No deprecation path** | Net-new addition. |
|
||||
|
||||
### Performance profile
|
||||
|
||||
**List endpoint (`GET /api/v1/errors`)**: Two queries.
|
||||
|
||||
1. COUNT query with same filters (no OFFSET/LIMIT). Complexity: O(n) scan of filtered rows on `error_tracker_errors` table. With typical production volumes (hundreds of unique errors), this is trivially fast.
|
||||
2. SELECT with filters + ORDER BY + LIMIT + OFFSET. Same O(n) scan profile. No joins — all data is in the single table for the list view.
|
||||
|
||||
No N+1 risk in the list endpoint: we return error-level data only, no occurrence preloading.
|
||||
|
||||
**Single error endpoint (`GET /api/v1/errors/:id`)**: Three queries.
|
||||
|
||||
1. `Repo.get!` on `error_tracker_errors` by ID. PK lookup is O(1).
|
||||
2. `Repo.preload` occurrences sorted by `inserted_at DESC`. This produces a single LEFT JOIN or an IN query (depends on preload strategy). For errors with hundreds of occurrences, the preload could return substantial data.
|
||||
3. A subquery COUNT on occurrences for `occurrence_count` (if not already preloaded). Can be merged with the preload.
|
||||
@@ -122,17 +128,20 @@ No paid resources consumed. The endpoints are internal HTTP handlers that only q
|
||||
**What**: New `lib/music_library/errors.ex` with two public functions and private query helpers.
|
||||
|
||||
**Functions**:
|
||||
|
||||
- `list_errors(opts)` — accepts keyword list: `[status: :unresolved, muted: false, search: "syntax error", limit: 50, offset: 0]`. Returns `%{errors: [...], total: n}`.
|
||||
- Filters: `status` → `where(status: ^status)`, `muted` → `where(muted: ^muted)` (boolean), `search` → `where(ilike(e.reason, ^"%#{search}%"))`
|
||||
- Count query: `Repo.aggregate/3` with same filters
|
||||
- List query: `order_by(desc: :last_occurrence_at)` + `limit/offset`
|
||||
- List query: `order_by(desc: :last_occurrence_at)` + `limit/offset`
|
||||
- Computed fields `occurrence_count` and `first_occurrence_at` are omitted from the list endpoint to keep queries simple. They are computed only in the single-error endpoint. This avoids a correlated subquery per row in the list.
|
||||
- `get_error!(id)` — fetches single error, preloads all occurrences sorted `desc: inserted_at`, computes `occurrence_count` and `first_occurrence_at`.
|
||||
|
||||
**Verification**:
|
||||
|
||||
```bash
|
||||
mix test test/music_library/errors_test.exs
|
||||
```
|
||||
|
||||
Write tests for: empty list, filtering by status, filtering by muted, search by reason substring, pagination (limit + offset), error not found raises, single error includes occurrence_count and occurrences.
|
||||
|
||||
---
|
||||
@@ -142,15 +151,18 @@ Write tests for: empty list, filtering by status, filtering by muted, search by
|
||||
**What**: New `lib/music_library_web/controllers/error_controller.ex` following `CollectionController` patterns.
|
||||
|
||||
**Actions**:
|
||||
|
||||
- `index(conn, params)` — parses `status`, `muted`, `search`, `limit`, `offset` from query params; calls `Errs.list_errors(opts)`; renders `:index`.
|
||||
- `show(conn, %{"id" => id})` — fetches error by integer ID; renders `:show`.
|
||||
|
||||
Use `parse_int/2` helper (same pattern as `CollectionController`).
|
||||
|
||||
**Verification**:
|
||||
|
||||
```bash
|
||||
mix test test/music_library_web/controllers/error_controller_test.exs
|
||||
```
|
||||
|
||||
Write tests for: auth required (401 without Bearer token), list returns JSON structure, filter params applied, single error returns JSON with occurrences.
|
||||
|
||||
---
|
||||
@@ -160,10 +172,12 @@ Write tests for: auth required (401 without Bearer token), list returns JSON str
|
||||
**What**: New `lib/music_library_web/controllers/error_json.ex` following `CollectionJSON` patterns.
|
||||
|
||||
**Render functions**:
|
||||
|
||||
- `index(%{errors: errors, total: total, limit: limit, offset: offset})` → `%{errors: [...], total: n, limit: n, offset: n}`
|
||||
- `show(%{error: error})` → single error with nested occurrences
|
||||
|
||||
**Serialization details**:
|
||||
|
||||
- `id` → integer
|
||||
- `status` → atom-to-string (`"resolved"` / `"unresolved"`)
|
||||
- `fingerprint` → hex string (already stored as hex string in SQLite TEXT column)
|
||||
@@ -175,9 +189,11 @@ Write tests for: auth required (401 without Bearer token), list returns JSON str
|
||||
- `occurrence_count` → integer
|
||||
|
||||
**Verification**:
|
||||
|
||||
```bash
|
||||
mix test test/music_library_web/controllers/error_controller_test.exs
|
||||
```
|
||||
|
||||
Tests from Step 2 already validate the JSON structure via `json_response/2`. Verify serialization of all fields including nested stacktrace lines.
|
||||
|
||||
---
|
||||
@@ -194,10 +210,13 @@ get "/errors/:id", ErrorController, :show
|
||||
Place after existing collection routes, before `assets` and `backup`.
|
||||
|
||||
**Verification**:
|
||||
|
||||
```bash
|
||||
mix phx.routes | grep errors
|
||||
```
|
||||
|
||||
Should show the two new API routes under `/api/v1/errors`. Also run existing tests to ensure no route conflicts:
|
||||
|
||||
```bash
|
||||
mix test
|
||||
```
|
||||
@@ -209,11 +228,14 @@ mix test
|
||||
**What**: Run the full test suite, update architecture docs.
|
||||
|
||||
**Verification**:
|
||||
|
||||
```bash
|
||||
mix test
|
||||
mix test test/music_library_web/controllers/error_controller_test.exs
|
||||
```
|
||||
|
||||
All tests pass including existing tests. Update `docs/architecture.md`:
|
||||
|
||||
- Add `Errors` context to the Contexts table
|
||||
- Add `ErrorController` to the Controllers table
|
||||
|
||||
@@ -222,6 +244,7 @@ All tests pass including existing tests. Update `docs/architecture.md`:
|
||||
### Production Changes
|
||||
|
||||
No manual production infrastructure changes required. The endpoints use:
|
||||
|
||||
- Existing `error_tracker_errors` and `error_tracker_occurrences` tables (already migrated)
|
||||
- Existing `require_api_token` plug (already configured with `API_TOKEN` env var)
|
||||
- Existing `MusicLibrary.Repo` (already configured)
|
||||
@@ -294,6 +317,7 @@ end
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
|
||||
- `fingerprint` is stored as TEXT in SQLite (the hex string from `Base.encode16/1`), despite the Ecto schema declaring `:binary`. Use hex strings when inserting.
|
||||
- `status` is an `Ecto.Enum` — pass atoms (`:resolved` / `:unresolved`).
|
||||
- `muted` is INTEGER in SQLite (`0`/`1`), but the Ecto schema accepts booleans.
|
||||
@@ -306,6 +330,7 @@ end
|
||||
## Implementation Notes
|
||||
|
||||
<!-- SECTION:NOTES:BEGIN -->
|
||||
|
||||
Implementation completed. All 5 steps executed:
|
||||
|
||||
1. **Context** (`lib/music_library/errors.ex`): Created `MusicLibrary.Errors` with `list_errors/1` (filtered + paginated) and `get_error/1` (returns `{:ok, error}` or `{:error, :not_found}`). Uses `MusicLibrary.Repo` (where error_tracker tables actually live, not TelemetryRepo). Private query helpers for status/muted/search filtering.
|
||||
@@ -330,6 +355,7 @@ Implementation completed. All 5 steps executed:
|
||||
## Final Summary
|
||||
|
||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||
|
||||
## Summary
|
||||
|
||||
Added two JSON API endpoints under `/api/v1/errors` to expose production error data from ErrorTracker, behind the existing Bearer token authentication.
|
||||
@@ -337,6 +363,7 @@ Added two JSON API endpoints under `/api/v1/errors` to expose production error d
|
||||
### What changed
|
||||
|
||||
**New files:**
|
||||
|
||||
- `lib/music_library/errors.ex` — Context with `list_errors/1` (filtered, paginated listing) and `get_error/1` (single error with preloaded occurrences, computed counts)
|
||||
- `lib/music_library_web/controllers/error_controller.ex` — Controller with `index/2` and `show/2` actions, following CollectionController patterns
|
||||
- `lib/music_library_web/controllers/error_json.ex` — JSON serializer for errors and occurrences, including stacktrace lines; also serves as Phoenix error renderer (404/500 JSON responses)
|
||||
@@ -344,6 +371,7 @@ Added two JSON API endpoints under `/api/v1/errors` to expose production error d
|
||||
- `test/music_library_web/controllers/error_controller_test.exs` — 10 tests: auth required (2), list/pagination/filter/search (6), single error with occurrences (1), 404 handling (1)
|
||||
|
||||
**Modified files:**
|
||||
|
||||
- `lib/music_library_web/router.ex` — Added `GET /api/v1/errors` and `GET /api/v1/errors/:id` routes
|
||||
- `docs/architecture.md` — Added Errors context and ErrorController entries
|
||||
|
||||
@@ -362,4 +390,5 @@ Added two JSON API endpoints under `/api/v1/errors` to expose production error d
|
||||
### Test results
|
||||
|
||||
All 900 tests pass (43 doctests, 857 existing + 10 new).
|
||||
|
||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||
|
||||
@@ -3,8 +3,8 @@ id: ML-163
|
||||
title: Create pi tools to fetch production errors
|
||||
status: Done
|
||||
assignee: []
|
||||
created_date: '2026-05-04 08:08'
|
||||
updated_date: '2026-05-04 12:22'
|
||||
created_date: "2026-05-04 08:08"
|
||||
updated_date: "2026-05-04 12:22"
|
||||
labels:
|
||||
- pi
|
||||
- ready
|
||||
@@ -17,6 +17,7 @@ ordinal: 5000
|
||||
## Description
|
||||
|
||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||
|
||||
Create `fetch_production_errors` and `fetch_production_error` pi tools that call the `/api/v1/errors` JSON API endpoint.
|
||||
|
||||
These tools are registered via `pi.registerTool()` in a pi extension. They make authenticated HTTP requests to the production server using the same HURL variable pattern established by `fetch_production_logs` (ML-160).
|
||||
@@ -24,18 +25,21 @@ These tools are registered via `pi.registerTool()` in a pi extension. They make
|
||||
### Tools
|
||||
|
||||
**`fetch_production_errors`** — List/filter production errors
|
||||
|
||||
- Parameters: `status` (optional: "resolved" | "unresolved"), `muted` (optional: boolean), `search` (optional: string, substring match on reason), `limit` (optional: number, default 50), `offset` (optional: number, default 0)
|
||||
- Calls `GET /api/v1/errors` with query params
|
||||
- Returns: formatted list of errors with counts and timestamps
|
||||
- Truncates output with `truncateTail` (50KB / 2000 lines)
|
||||
|
||||
**`fetch_production_error`** — Get a single error with full details
|
||||
|
||||
- Parameters: `id` (required: integer)
|
||||
- Calls `GET /api/v1/errors/:id`
|
||||
- Returns: error details with all occurrences, stacktraces, and context
|
||||
- Truncates output with `truncateTail`
|
||||
|
||||
### Tool guidelines (promptGuidelines)
|
||||
|
||||
- Use fetch_production_errors when investigating what errors are occurring in production
|
||||
- Use fetch_production_error when you need full details on a specific error, including stacktraces and context
|
||||
- Start with a small limit (e.g., 20) and filter by status or search before fetching large result sets
|
||||
@@ -43,15 +47,18 @@ These tools are registered via `pi.registerTool()` in a pi extension. They make
|
||||
### Auth
|
||||
|
||||
Uses the same HURL variable pattern as `fetch_production_logs`:
|
||||
|
||||
- `PI_API_TOKEN` — Bearer token for API auth
|
||||
- `PI_SERVICE_FQDN_WEB` — Production domain
|
||||
|
||||
They need to be configured in the pi environment as local secrets.
|
||||
|
||||
<!-- SECTION:DESCRIPTION:END -->
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
<!-- SECTION:PLAN:BEGIN -->
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Objective alignment
|
||||
@@ -80,27 +87,27 @@ Create two pi tools — `fetch_production_errors` and `fetch_production_error`
|
||||
|
||||
### Architecture impact analysis
|
||||
|
||||
| Touchpoint | Impact |
|
||||
|---|---|
|
||||
| `.pi/extensions/prod-errors/index.ts` | **New file** — ~200 lines: two tool registrations, `resolveVar()` helper, `fetchErrors()` and `fetchError()` HTTP helpers, formatting functions |
|
||||
| `.pi/extensions/prod-errors/package.json` | **New file** — minimal `{ name, private, description }` for extension directory scope |
|
||||
| `.pi/extensions/prod-logs/index.ts` | **No change** — error tools live in their own extension |
|
||||
| Elixir modules, schemas, controllers | **No change** — these are the responsibility of ML-162 |
|
||||
| Router, PubSub, supervision tree | **No change** |
|
||||
| Config / env vars | **Two new env vars required in pi's environment**: `PI_API_TOKEN` (Bearer token, must match production's `API_TOKEN`), `PI_SERVICE_FQDN_WEB` (production domain, e.g., `https://musiclibrary.example.com`). These are pi-local secrets, not server-side config. |
|
||||
| Existing pi tools | **No change** — `fetch_production_logs` continues to use `PI_COOLIFY_*` vars, no overlap |
|
||||
| Touchpoint | Impact |
|
||||
| ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `.pi/extensions/prod-errors/index.ts` | **New file** — ~200 lines: two tool registrations, `resolveVar()` helper, `fetchErrors()` and `fetchError()` HTTP helpers, formatting functions |
|
||||
| `.pi/extensions/prod-errors/package.json` | **New file** — minimal `{ name, private, description }` for extension directory scope |
|
||||
| `.pi/extensions/prod-logs/index.ts` | **No change** — error tools live in their own extension |
|
||||
| Elixir modules, schemas, controllers | **No change** — these are the responsibility of ML-162 |
|
||||
| Router, PubSub, supervision tree | **No change** |
|
||||
| Config / env vars | **Two new env vars required in pi's environment**: `PI_API_TOKEN` (Bearer token, must match production's `API_TOKEN`), `PI_SERVICE_FQDN_WEB` (production domain, e.g., `https://musiclibrary.example.com`). These are pi-local secrets, not server-side config. |
|
||||
| Existing pi tools | **No change** — `fetch_production_logs` continues to use `PI_COOLIFY_*` vars, no overlap |
|
||||
|
||||
### Performance profile
|
||||
|
||||
| Aspect | Characteristic |
|
||||
|---|---|
|
||||
| **Runtime complexity** | O(n) for formatting error list output (one pass over error array). O(n + m) for formatting single error detail (one pass over occurrences + stacktrace lines). Truncation is O(bytes) for byte counting. |
|
||||
| **Network** | One HTTP GET per tool call. No retries in the tool handler (the LLM can retry by calling the tool again). |
|
||||
| **Memory** | Response JSON is parsed in memory. For a list of 50 errors, response size is ~5-20KB. For a single error with 100 occurrences each with full stacktraces, response could reach ~100-500KB. Truncation via `truncateTail` caps output at 50KB/2000 lines. If a noisy error generates thousands of occurrences, the API response could be megabytes — this is accepted per the ML-162 spec (all occurrences returned). A future follow-up could add `limit`/`offset` params for occurrences in the single-error endpoint. |
|
||||
| **Latency** | Dominated by network round-trip to production server (typically 50-500ms). Local JSON parsing and formatting is negligible. |
|
||||
| **Database** | No direct database access — all queries are on the server side (ML-162). |
|
||||
| **N+1 risk** | None — a single API call per tool invocation. The server-side API (ML-162) handles preloading/prevention of N+1 queries. |
|
||||
| **Abort support** | Both tools pass `signal` to `fetch()`, so the LLM can cancel in-flight requests. |
|
||||
| Aspect | Characteristic |
|
||||
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Runtime complexity** | O(n) for formatting error list output (one pass over error array). O(n + m) for formatting single error detail (one pass over occurrences + stacktrace lines). Truncation is O(bytes) for byte counting. |
|
||||
| **Network** | One HTTP GET per tool call. No retries in the tool handler (the LLM can retry by calling the tool again). |
|
||||
| **Memory** | Response JSON is parsed in memory. For a list of 50 errors, response size is ~5-20KB. For a single error with 100 occurrences each with full stacktraces, response could reach ~100-500KB. Truncation via `truncateTail` caps output at 50KB/2000 lines. If a noisy error generates thousands of occurrences, the API response could be megabytes — this is accepted per the ML-162 spec (all occurrences returned). A future follow-up could add `limit`/`offset` params for occurrences in the single-error endpoint. |
|
||||
| **Latency** | Dominated by network round-trip to production server (typically 50-500ms). Local JSON parsing and formatting is negligible. |
|
||||
| **Database** | No direct database access — all queries are on the server side (ML-162). |
|
||||
| **N+1 risk** | None — a single API call per tool invocation. The server-side API (ML-162) handles preloading/prevention of N+1 queries. |
|
||||
| **Abort support** | Both tools pass `signal` to `fetch()`, so the LLM can cancel in-flight requests. |
|
||||
|
||||
### Benchmarking requirements
|
||||
|
||||
@@ -131,9 +138,11 @@ No paid resources consumed. The tools make HTTP requests to the project's own pr
|
||||
```
|
||||
|
||||
**Verification**:
|
||||
|
||||
```bash
|
||||
ls -la .pi/extensions/prod-errors/package.json
|
||||
```
|
||||
|
||||
File must exist with valid JSON.
|
||||
|
||||
---
|
||||
@@ -144,11 +153,12 @@ File must exist with valid JSON.
|
||||
|
||||
1. **Imports**: `ExtensionAPI`, `truncateTail`, `formatSize`, `DEFAULT_MAX_BYTES`, `DEFAULT_MAX_LINES` from `@mariozechner/pi-coding-agent`; `Type` from `typebox`.
|
||||
|
||||
2. **`resolveVar(name: string): string | undefined`** — Reads `process.env[`PI_${name.toUpperCase()}`]`. Identical to the helper in `prod-logs/index.ts`. Shared between both tools for reading `api_token` and `service_fqdn_web`.
|
||||
2. **`resolveVar(name: string): string | undefined`** — Reads `process.env[`PI\_${name.toUpperCase()}`]`. Identical to the helper in `prod-logs/index.ts`. Shared between both tools for reading `api_token` and `service_fqdn_web`.
|
||||
|
||||
3. **`buildUrl(base: string, path: string, params?: Record<string, string>): string`** — Constructs the full API URL. Strips trailing slash from base, prepends `https://` if no protocol present, appends path and query params.
|
||||
|
||||
4. **`formatErrorListItem(error: object, index: number): string`** — Formats a single error from the list endpoint into a human-readable block. Uses only fields returned by the list endpoint (note: `occurrence_count` and `first_occurrence_at` are intentionally absent — they are not included in the list endpoint per ML-162 to avoid correlated subqueries):
|
||||
|
||||
```
|
||||
#{index} [{status}] {kind}: {reason}
|
||||
Source: {source_line} — {source_function}
|
||||
@@ -158,6 +168,7 @@ File must exist with valid JSON.
|
||||
```
|
||||
|
||||
5. **`formatErrorDetail(error: object): string`** — Formats a single error with all occurrences into a detailed block:
|
||||
|
||||
```
|
||||
Error #{id}: {kind}
|
||||
────────────────────────────────────────────
|
||||
@@ -183,6 +194,7 @@ File must exist with valid JSON.
|
||||
6. **`applyOutputTruncation(output: string): string`** — Wraps `truncateTail` with `DEFAULT_MAX_BYTES` and `DEFAULT_MAX_LINES`. Appends truncation note if truncated, matching the `fetch_production_logs` format. Note: truncation may cut mid-line in long stacktraces — this is acceptable for a 50KB output cap; the truncation note guides the LLM to use more specific filters if needed.
|
||||
|
||||
**Verification**:
|
||||
|
||||
- No verification at this step — functions are tested implicitly in Steps 3 and 4 when the tools are called. To unit-test in isolation, import and call helpers from the pi eval REPL after `/reload`.
|
||||
|
||||
---
|
||||
@@ -192,6 +204,7 @@ File must exist with valid JSON.
|
||||
**What**: Register a `pi.registerTool()` call in the extension's default export function. The tool calls `GET /api/v1/errors` with query parameters and returns formatted text.
|
||||
|
||||
**Tool specification**:
|
||||
|
||||
- `name`: `"fetch_production_errors"`
|
||||
- `label`: `"Fetch Production Errors"`
|
||||
- `description`: Explains when to use this tool (investigating production errors, understanding error frequency, filtering by status/muted/search). Mentions the 50KB/2000-line truncation limit.
|
||||
@@ -209,6 +222,7 @@ File must exist with valid JSON.
|
||||
- `offset` — `Type.Optional(Type.Number())`, default 0, pagination offset
|
||||
|
||||
**Execute handler logic**:
|
||||
|
||||
1. **Early abort check**: If `signal?.aborted`, return `{ content: [{ type: "text", text: "Cancelled" }] }`.
|
||||
2. **Validate credentials**: Read `PI_API_TOKEN` and `PI_SERVICE_FQDN_WEB` via `resolveVar()`. If either is missing, return an error listing which env vars are not set.
|
||||
3. **Build URL**: Construct `{base}/api/v1/errors` with query params for all non-nil parameters (`status`, `muted`, `search`, `limit`, `offset`).
|
||||
@@ -222,6 +236,7 @@ File must exist with valid JSON.
|
||||
11. **Return**: `{ content: [{ type: "text", text: output }], details: { total, count, offset, limit } }`.
|
||||
|
||||
**Verification**:
|
||||
|
||||
1. Run `/reload` in pi to hot-reload the extension.
|
||||
2. Ask the LLM: "What tools are available for production errors?" — it should describe `fetch_production_errors` and `fetch_production_error`.
|
||||
3. With a locally running Phoenix server (ML-162 prerequisite), ask the LLM: "List the 5 most recent unresolved production errors" — verify the tool is called and returns formatted error list.
|
||||
@@ -239,6 +254,7 @@ File must exist with valid JSON.
|
||||
**What**: Register a second `pi.registerTool()` call. The tool calls `GET /api/v1/errors/:id` and returns formatted detail text.
|
||||
|
||||
**Tool specification**:
|
||||
|
||||
- `name`: `"fetch_production_error"`
|
||||
- `label`: `"Fetch Production Error Detail"`
|
||||
- `description`: Explains when to use this tool (getting full details on a specific error, including all occurrences, stacktraces, context, and breadcrumbs). Says to get the error ID from `fetch_production_errors` first.
|
||||
@@ -251,6 +267,7 @@ File must exist with valid JSON.
|
||||
- `id` — `Type.Number()`, required, the error ID (integer) from the list endpoint
|
||||
|
||||
**Execute handler logic**:
|
||||
|
||||
1. **Early abort check**: Same as Step 3.
|
||||
2. **Validate credentials**: Same as Step 3.
|
||||
3. **Build URL**: `{base}/api/v1/errors/{id}` (no query params).
|
||||
@@ -263,6 +280,7 @@ File must exist with valid JSON.
|
||||
10. **Return**: `{ content: [{ type: "text", text: output }], details: { errorId: id, occurrenceCount: data.error.occurrence_count } }`.
|
||||
|
||||
**Verification**:
|
||||
|
||||
1. Get an error ID from the list tool (Step 3 verification #3).
|
||||
2. Ask the LLM: "Show me full details for error ID X" — verify the tool returns formatted detail with occurrences, stacktraces, context, and breadcrumbs.
|
||||
3. Ask the LLM for a non-existent error ID (e.g., 99999) — verify the tool returns a "not found" message.
|
||||
@@ -275,14 +293,17 @@ File must exist with valid JSON.
|
||||
**What**: Set `PI_API_TOKEN` and `PI_SERVICE_FQDN_WEB` in pi's environment. These are pi-local secrets — they are read by the extension at runtime via `resolveVar()`.
|
||||
|
||||
**Production values**:
|
||||
|
||||
- `PI_API_TOKEN` — must match the `API_TOKEN` environment variable configured on the production server (set in Coolify or `runtime.exs`)
|
||||
- `PI_SERVICE_FQDN_WEB` — the production domain, e.g., `https://musiclibrary.example.com` (no trailing slash)
|
||||
|
||||
**Development values** (for local testing):
|
||||
|
||||
- `PI_API_TOKEN` — match the `API_TOKEN` set in `config/runtime.exs` or `dev.exs`
|
||||
- `PI_SERVICE_FQDN_WEB` — `http://localhost:4000`
|
||||
|
||||
**Verification**:
|
||||
|
||||
1. Verify the env vars are set: `echo $PI_API_TOKEN` and `echo $PI_SERVICE_FQDN_WEB` in a shell where pi runs.
|
||||
2. Run the extension tool and confirm it authenticates successfully (no 401 errors).
|
||||
3. Verify that calling the tool without the env vars set produces the clear "missing environment variables" error from Step 3.
|
||||
@@ -294,6 +315,7 @@ File must exist with valid JSON.
|
||||
**What**: End-to-end test of both tools with the production (or local) API.
|
||||
|
||||
**Verification**:
|
||||
|
||||
1. Run `/reload` in pi.
|
||||
2. Ask the LLM: "What production errors exist?" — it should call `fetch_production_errors` with default parameters.
|
||||
3. Ask the LLM: "Show me details for the most recent unresolved error." — it should chain: call `fetch_production_errors` with `status: "unresolved"` and `limit: 1`, extract the ID, then call `fetch_production_error` with that ID.
|
||||
@@ -305,11 +327,11 @@ File must exist with valid JSON.
|
||||
|
||||
### Production Changes
|
||||
|
||||
| Change | Detail | Rollout | Rollback |
|
||||
|---|---|---|---|
|
||||
| **New env var: `PI_API_TOKEN`** | Must be set in pi's runtime environment. Value must match the `API_TOKEN` env var on the production server (configured in `runtime.exs`). This is a pi-local secret — it does not change any server configuration. | Set in the shell/process that runs pi. If using a `.env` file or pi's settings, add it there. | Unset or change the env var. No server-side change needed. |
|
||||
| **New env var: `PI_SERVICE_FQDN_WEB`** | Must be set in pi's runtime environment. The production domain (with `https://` prefix, no trailing slash). This is a pi-local configuration value. | Set alongside `PI_API_TOKEN`. | Unset or change the env var. No server-side change needed. |
|
||||
| **ML-162 prerequisite** | The `/api/v1/errors` and `/api/v1/errors/:id` endpoints must exist on the production server. ML-162 is a prerequisite task. | Deploy ML-162 first. | Roll back ML-162 deployment (revert routes). |
|
||||
| Change | Detail | Rollout | Rollback |
|
||||
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
|
||||
| **New env var: `PI_API_TOKEN`** | Must be set in pi's runtime environment. Value must match the `API_TOKEN` env var on the production server (configured in `runtime.exs`). This is a pi-local secret — it does not change any server configuration. | Set in the shell/process that runs pi. If using a `.env` file or pi's settings, add it there. | Unset or change the env var. No server-side change needed. |
|
||||
| **New env var: `PI_SERVICE_FQDN_WEB`** | Must be set in pi's runtime environment. The production domain (with `https://` prefix, no trailing slash). This is a pi-local configuration value. | Set alongside `PI_API_TOKEN`. | Unset or change the env var. No server-side change needed. |
|
||||
| **ML-162 prerequisite** | The `/api/v1/errors` and `/api/v1/errors/:id` endpoints must exist on the production server. ML-162 is a prerequisite task. | Deploy ML-162 first. | Roll back ML-162 deployment (revert routes). |
|
||||
|
||||
No database migrations, DNS changes, firewall rules, or service provisioning are needed for this task.
|
||||
|
||||
@@ -328,12 +350,12 @@ The tool descriptions, `promptSnippet`, and `promptGuidelines` are the primary d
|
||||
|
||||
### Dependencies
|
||||
|
||||
| Dependency | Status |
|
||||
|---|---|
|
||||
| Dependency | Status |
|
||||
| ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **ML-162** (server-side API endpoint) | **Required.** The tools cannot function without the `/api/v1/errors` endpoints. ML-162 must be complete and deployed before ML-163 can be verified against production. For development, run the Phoenix server locally with the ML-162 routes. |
|
||||
| `typebox` | Already available as a pi built-in import. |
|
||||
| `@mariozechner/pi-coding-agent` | Already available. Provides `truncateTail`, `formatSize`, `DEFAULT_MAX_BYTES`, `DEFAULT_MAX_LINES`, `ExtensionAPI` type. |
|
||||
| No new npm dependencies | The extension uses only `fetch()` (Node.js built-in) and pi built-ins. |
|
||||
| `typebox` | Already available as a pi built-in import. |
|
||||
| `@mariozechner/pi-coding-agent` | Already available. Provides `truncateTail`, `formatSize`, `DEFAULT_MAX_BYTES`, `DEFAULT_MAX_LINES`, `ExtensionAPI` type. |
|
||||
| No new npm dependencies | The extension uses only `fetch()` (Node.js built-in) and pi built-ins. |
|
||||
|
||||
---
|
||||
|
||||
@@ -342,6 +364,7 @@ The tool descriptions, `promptSnippet`, and `promptGuidelines` are the primary d
|
||||
To test the tools locally, seed error data via the ML-162 test fixtures (`test/support/fixtures/errors_fixtures.ex`), or use the production server if ML-162 is deployed. The tools themselves are stateless HTTP clients — they don't need their own test data.
|
||||
|
||||
To verify locally without the production server:
|
||||
|
||||
1. Complete ML-162 (API endpoint + fixtures).
|
||||
2. Run `mix test` to populate test errors (or manually insert via `MusicLibrary.Repo`).
|
||||
3. Start the Phoenix server: `mix phx.server`.
|
||||
@@ -352,6 +375,7 @@ To verify locally without the production server:
|
||||
## Final Summary
|
||||
|
||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||
|
||||
## Summary
|
||||
|
||||
Created `fetch_production_errors` and `fetch_production_error` pi tools in a new `.pi/extensions/prod-errors/` extension. The tools call the `/api/v1/errors` JSON API (built in ML-162) with Bearer token authentication, using the same env-var pattern established by `fetch_production_logs` (ML-160).
|
||||
@@ -359,10 +383,12 @@ Created `fetch_production_errors` and `fetch_production_error` pi tools in a new
|
||||
### What changed
|
||||
|
||||
**New files:**
|
||||
|
||||
- `.pi/extensions/prod-errors/package.json` — Minimal extension package
|
||||
- `.pi/extensions/prod-errors/index.ts` — ~330 lines: two tool registrations, HTTP helpers, formatting functions
|
||||
|
||||
**Modified files:**
|
||||
|
||||
- `docs/production-infrastructure.md` — Added pi coding agent tools section with env var documentation
|
||||
|
||||
### Tools
|
||||
@@ -381,4 +407,5 @@ Created `fetch_production_errors` and `fetch_production_error` pi tools in a new
|
||||
### Test results
|
||||
|
||||
All 900 Elixir tests pass (no regressions). Pi tools verified by `/reload` in pi.
|
||||
|
||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||
|
||||
Reference in New Issue
Block a user