feat: AI ticket analyzer (phases 1-6)
Multi-stage LLM pipeline that produces structured analyses of Autotask tickets from local Postgres. Migration 069 + Zod schemas, Stage 0 preprocessor, IT Glue redaction + search, Anthropic SDK wrapper, Stages 1/3/4 (Haiku/Sonnet/Opus), pipeline + cost circuit breaker, job worker (opt-in autostart), 6 API routes, 3 frontend pages, share-row persistence (email send deferred to phase 7). 128 vitest tests, tsc clean. Build journal in docs/wulf-pulse-ticket-analyzer-build-notes.md. Sync: adds syncTicketNotes() + ticket_notes to ordered/date-filtered entities so the analyzer's local mirror stays current via scheduler. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ea3471d38d
commit
8f8b5ab7be
53 changed files with 9377 additions and 33 deletions
116
migrations/069_create_analyzer_tables.sql
Normal file
116
migrations/069_create_analyzer_tables.sql
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
-- AI Ticket Analyzer feature
|
||||
-- Stores versioned analyses of Autotask tickets, share log, and an on-demand job queue.
|
||||
-- See docs/wulf-pulse-ticket-analyzer-prompt.md for the feature spec.
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||
|
||||
-- =============================================================================
|
||||
-- analyzer_analyses
|
||||
-- One row per completed (or failed) analysis; versioned per ticket_number.
|
||||
-- =============================================================================
|
||||
CREATE TABLE IF NOT EXISTS analyzer_analyses (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
ticket_number TEXT NOT NULL,
|
||||
autotask_ticket_id BIGINT NOT NULL,
|
||||
analysis_version INT NOT NULL,
|
||||
content_hash_at_analysis TEXT NOT NULL,
|
||||
triggered_by_user_id TEXT REFERENCES "user"(id) ON DELETE SET NULL,
|
||||
triggered_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
status TEXT NOT NULL DEFAULT 'pending'
|
||||
CHECK (status IN ('pending','running','complete','failed')),
|
||||
completed_at TIMESTAMPTZ,
|
||||
|
||||
-- Model usage
|
||||
haiku_used BOOLEAN NOT NULL DEFAULT false,
|
||||
sonnet_used BOOLEAN NOT NULL DEFAULT false,
|
||||
opus_used BOOLEAN NOT NULL DEFAULT false,
|
||||
total_input_tokens INT NOT NULL DEFAULT 0,
|
||||
total_output_tokens INT NOT NULL DEFAULT 0,
|
||||
estimated_cost_usd NUMERIC(10,4) NOT NULL DEFAULT 0,
|
||||
|
||||
-- Structured output (each LLM stage's parsed JSON)
|
||||
summary TEXT,
|
||||
timeline JSONB,
|
||||
what_was_done JSONB,
|
||||
what_should_have_been_done JSONB,
|
||||
gaps JSONB,
|
||||
next_step TEXT,
|
||||
next_step_rationale TEXT,
|
||||
post_resolution_analysis TEXT,
|
||||
confidence_score NUMERIC(3,2),
|
||||
needs_human_review BOOLEAN NOT NULL DEFAULT false,
|
||||
human_review_reasons JSONB,
|
||||
|
||||
itglue_docs_referenced JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
|
||||
-- Debugging / observability
|
||||
model_traces JSONB,
|
||||
filtered_noise_count INT NOT NULL DEFAULT 0,
|
||||
error_message TEXT,
|
||||
|
||||
CONSTRAINT analyzer_analyses_version_unique UNIQUE (ticket_number, analysis_version)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_analyzer_analyses_ticket_version
|
||||
ON analyzer_analyses (ticket_number, analysis_version DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_analyzer_analyses_triggered_at
|
||||
ON analyzer_analyses (triggered_at DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_analyzer_analyses_needs_review
|
||||
ON analyzer_analyses (needs_human_review)
|
||||
WHERE needs_human_review = true;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_analyzer_analyses_autotask_ticket_id
|
||||
ON analyzer_analyses (autotask_ticket_id);
|
||||
|
||||
COMMENT ON TABLE analyzer_analyses IS 'Versioned AI analyses of Autotask tickets; one row per completed analysis run.';
|
||||
COMMENT ON COLUMN analyzer_analyses.content_hash_at_analysis IS 'sha256 of canonical-JSON of (tagged_events, ticket_status, ticket_priority, queue) at analysis time. Used for idempotency.';
|
||||
COMMENT ON COLUMN analyzer_analyses.filtered_noise_count IS 'Count of workflow-rule and notification-email notes stripped during pre-processing.';
|
||||
COMMENT ON COLUMN analyzer_analyses.itglue_docs_referenced IS 'IDs/names of IT Glue docs cited; doc bodies are NEVER stored here (security).';
|
||||
|
||||
-- =============================================================================
|
||||
-- analyzer_shares
|
||||
-- Audit log of share-by-email events for any analysis.
|
||||
-- =============================================================================
|
||||
CREATE TABLE IF NOT EXISTS analyzer_shares (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
analysis_id UUID NOT NULL REFERENCES analyzer_analyses(id) ON DELETE CASCADE,
|
||||
shared_by_user_id TEXT NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
|
||||
shared_with_email TEXT NOT NULL,
|
||||
note TEXT,
|
||||
shared_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
viewed_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_analyzer_shares_analysis_id ON analyzer_shares (analysis_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_analyzer_shares_shared_by ON analyzer_shares (shared_by_user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_analyzer_shares_shared_at ON analyzer_shares (shared_at DESC);
|
||||
|
||||
COMMENT ON TABLE analyzer_shares IS 'Audit log of analyses shared by email. Recipient domains validated against ALLOWED_SHARE_DOMAINS at write time.';
|
||||
|
||||
-- =============================================================================
|
||||
-- analyzer_jobs
|
||||
-- On-demand pipeline queue. A worker initialised at server start polls this table.
|
||||
-- =============================================================================
|
||||
CREATE TABLE IF NOT EXISTS analyzer_jobs (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
ticket_number TEXT NOT NULL,
|
||||
queued_by_user_id TEXT REFERENCES "user"(id) ON DELETE SET NULL,
|
||||
status TEXT NOT NULL DEFAULT 'queued'
|
||||
CHECK (status IN ('queued','fetching','triaging','itglue','analyzing','deep_review','complete','failed')),
|
||||
result_analysis_id UUID REFERENCES analyzer_analyses(id) ON DELETE SET NULL,
|
||||
queued_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
started_at TIMESTAMPTZ,
|
||||
finished_at TIMESTAMPTZ,
|
||||
error_message TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_analyzer_jobs_status_queued
|
||||
ON analyzer_jobs (status, queued_at)
|
||||
WHERE status IN ('queued','fetching','triaging','itglue','analyzing','deep_review');
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_analyzer_jobs_ticket_number ON analyzer_jobs (ticket_number);
|
||||
CREATE INDEX IF NOT EXISTS idx_analyzer_jobs_queued_at ON analyzer_jobs (queued_at DESC);
|
||||
|
||||
COMMENT ON TABLE analyzer_jobs IS 'On-demand analyzer pipeline jobs. Polled by a single worker that initialises at server start.';
|
||||
Loading…
Add table
Add a link
Reference in a new issue