generated from coulomb/repo-seed
feat(P5): IHF Phase 5 complete — agent-assisted distillation
Some checks failed
Test / test (push) Has been cancelled
Some checks failed
Test / test (push) Has been cancelled
Adds bounded AI support to the IHF governance loop. All AI outputs are attributed (model_ref), reviewable (AgentReviewRecord), and reversible. No autonomous decisions; no silent requirement promotion. - T01: Schema — agent_proposals, agent_review_records, confidence_annotations (migration 1743379200) - T02: AgentProposalsController (index/show/accept/reject, idempotent review guard), global nav "Agent" link - T03: SummarizeClusterAction — Claude API cluster summary on widget show - T04: DraftRequirementAction — AI requirement draft; acceptance creates RequirementCandidate (human-gated) - T05: DetectDuplicatesAction — duplicate_flag proposal on candidate show - T06: DetectPolicySensitivityAction — policy_flag with ConfidenceAnnotations per concern scope - T07: ProposeImplementationAction — impl_proposal from decision show - T08: AgentAuditDashboardAction — autoRefresh; KPI row, unreviewed queue, recent proposals, attribution log matrix - T09: integration tests, SCOPE.md updated, phase5-summary.md, flake.nix adds http-conduit/aeson/string-conversions Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,7 +9,8 @@ import Generated.Types
|
||||
import IHP.Prelude
|
||||
import IHP.ControllerPrelude
|
||||
import Data.Aeson (toJSON, object, (.=))
|
||||
import Application.Helper.Controller (isInRegression, widgetCycleCounts)
|
||||
import Application.Helper.Controller (isInRegression, widgetCycleCounts, callClaudeApi)
|
||||
import Data.List (intercalate)
|
||||
|
||||
instance Controller WidgetsController where
|
||||
beforeAction = ensureIsUser
|
||||
@@ -121,3 +122,65 @@ instance Controller WidgetsController where
|
||||
|> createRecord
|
||||
setSuccessMessage "Widget updated"
|
||||
redirectTo ShowWidgetAction { widgetId = widget.id }
|
||||
|
||||
-- T03: Summarize feedback cluster via Claude API
|
||||
action SummarizeClusterAction { widgetId } = do
|
||||
annotations <- query @Annotation
|
||||
|> filterWhere (#widgetId, widgetId)
|
||||
|> orderByDesc #createdAt
|
||||
|> limit 20
|
||||
|> fetch
|
||||
threads <- query @AnnotationThread
|
||||
|> filterWhere (#widgetId, widgetId)
|
||||
|> orderByDesc #createdAt
|
||||
|> limit 20
|
||||
|> fetch
|
||||
let annLines = map (\a -> "[" <> a.category <> "/" <> a.severity <> "] " <> a.body) annotations
|
||||
threadLines = map (\t -> "[thread] " <> t.title <> ": " <> fromMaybe "" t.description) threads
|
||||
userMsg = intercalate "\n" (annLines <> threadLines)
|
||||
result <- liftIO $ callClaudeApi
|
||||
"You are a distillation assistant for a governed interaction hub. Summarize the following user feedback cluster into a concise, actionable summary (2\x20134 sentences). Be factual and neutral."
|
||||
userMsg
|
||||
300
|
||||
case result of
|
||||
Left err -> do
|
||||
setErrorMessage ("AI summarization failed: " <> err)
|
||||
redirectTo ShowWidgetAction { widgetId }
|
||||
Right content -> do
|
||||
newRecord @AgentProposal
|
||||
|> set #proposalType "summary"
|
||||
|> set #sourceWidgetId (Just widgetId)
|
||||
|> set #content content
|
||||
|> set #modelRef "claude-sonnet-4-6"
|
||||
|> set #status "pending"
|
||||
|> createRecord
|
||||
setSuccessMessage "Summary proposal created"
|
||||
redirectTo ShowWidgetAction { widgetId }
|
||||
|
||||
-- T04: Draft a requirement candidate via Claude API
|
||||
action DraftRequirementAction { widgetId } = do
|
||||
annotations <- query @Annotation
|
||||
|> filterWhere (#widgetId, widgetId)
|
||||
|> orderByDesc #createdAt
|
||||
|> limit 20
|
||||
|> fetch
|
||||
let annLines = map (\a -> "[" <> a.category <> "/" <> a.severity <> "] " <> a.body) annotations
|
||||
userMsg = intercalate "\n" annLines
|
||||
result <- liftIO $ callClaudeApi
|
||||
"You are a requirements analyst. Given these friction annotations, draft a single structured requirement candidate. Respond with JSON: {\"title\": \"...\", \"description\": \"...\"}."
|
||||
userMsg
|
||||
400
|
||||
case result of
|
||||
Left err -> do
|
||||
setErrorMessage ("AI draft failed: " <> err)
|
||||
redirectTo ShowWidgetAction { widgetId }
|
||||
Right content -> do
|
||||
newRecord @AgentProposal
|
||||
|> set #proposalType "requirement_draft"
|
||||
|> set #sourceWidgetId (Just widgetId)
|
||||
|> set #content content
|
||||
|> set #modelRef "claude-sonnet-4-6"
|
||||
|> set #status "pending"
|
||||
|> createRecord
|
||||
setSuccessMessage "Requirement draft proposal created"
|
||||
redirectTo ShowWidgetAction { widgetId }
|
||||
|
||||
Reference in New Issue
Block a user