package app import ( "context" "fmt" "time" "gitea.maximumdirect.net/eric/weatherreporter/internal/briefing" "gitea.maximumdirect.net/eric/weatherreporter/internal/collect" "gitea.maximumdirect.net/eric/weatherreporter/internal/fileutil" "gitea.maximumdirect.net/eric/weatherreporter/internal/generatedtext" "gitea.maximumdirect.net/eric/weatherreporter/internal/module" "gitea.maximumdirect.net/eric/weatherreporter/internal/promptdebug" "gitea.maximumdirect.net/eric/weatherreporter/internal/promptexec" "gitea.maximumdirect.net/eric/weatherreporter/internal/promptinput" "gitea.maximumdirect.net/eric/weatherreporter/internal/report" "gitea.maximumdirect.net/eric/weatherreporter/internal/state" ) type promptReportRequest struct { GenerateRequest Resolved report.Resolved Collection collect.Result Inspection PromptInspectionResult DebugWriter *promptdebug.PromptDebugWriter noNotify bool } func generatePromptReport(ctx context.Context, req promptReportRequest) (*ReportResult, error) { workflow, err := newPromptReportWorkflow(ctx, req) if err != nil { return nil, err } if err := workflow.buildInputs(); err != nil { return workflow.result, err } execution, executeErr := workflow.executePrompt() if executeErr != nil { return workflow.result, workflow.handleExecutionFailure(executeErr) } if execution == nil { err := promptexec.NewError(promptexec.Generation, "prompt executor returned no execution", nil) if saveErr := workflow.persistOperationalExecutionFailure(err); saveErr != nil { return workflow.result, saveErr } return workflow.result, workflow.reportError("execute prompt", err) } if err := workflow.persistExecutionDebug(*execution); err != nil { return workflow.result, err } if execution.Validation.Status != promptexec.ValidationPassed && execution.Validation.Status != promptexec.ValidationFailed { err := promptexec.NewError(promptexec.OperationalValidation, "prompt execution did not complete validation", nil) if saveErr := workflow.persistOperationalExecutionFailure(err); saveErr != nil { return workflow.result, saveErr } return workflow.result, workflow.reportError("validate prompt execution", err) } if err := workflow.persistCompletedExecution(*execution); err != nil { return workflow.result, err } if execution.Validation.Status == promptexec.ValidationFailed { err := promptexec.NewError(promptexec.ValidationRejected, "prompt output did not satisfy its schema", nil) return workflow.result, workflow.reportError("validate prompt execution", err) } rendered, err := workflow.persistGeneratedContent(execution.RawOutput) if err != nil { return workflow.result, err } return workflow.finalizeReport(rendered) } type promptReportWorkflow struct { ctx context.Context req promptReportRequest store state.Store result *ReportResult metadata state.Metadata briefingMetadata briefing.Metadata reportFacts ReportFacts moduleSnapshot module.Snapshot dataPackageBytes []byte handler generatedtext.Handler executionArtifact state.PromptExecutionArtifact debugRef promptdebug.PromptDebugRef prepared bool callbackFailed bool } func newPromptReportWorkflow(ctx context.Context, req promptReportRequest) (*promptReportWorkflow, error) { if req.Collection.Bundle == nil { return nil, fmt.Errorf("collected weather bundle is required") } store := req.Store var err error if store == nil { store, err = defaultStore(req.Config) if err != nil { return nil, err } } return &promptReportWorkflow{ctx: ctx, req: req, store: store}, nil } func (w *promptReportWorkflow) buildInputs() error { paths, err := w.store.Paths(w.req.Resolved) if err != nil { return err } w.result = &ReportResult{} w.reportFacts, err = BuildReportFacts(ModuleSnapshotRequest{Config: w.req.Config, Resolved: w.req.Resolved}, w.req.Collection.Bundle) if err != nil { return generatedReportError(w.req.Resolved, w.req.Resolved.Metadata().RunID, "build report facts", err) } w.moduleSnapshot, err = BuildModuleSnapshotFromFacts(ModuleSnapshotRequest{Config: w.req.Config, Resolved: w.req.Resolved}, w.reportFacts) if err != nil { return generatedReportError(w.req.Resolved, w.req.Resolved.Metadata().RunID, "build module snapshot", err) } moduleSnapshotPath, err := w.store.SaveModuleSnapshot(w.ctx, w.req.Resolved, w.moduleSnapshot) if err != nil { return err } w.result.ModuleSnapshot = w.moduleSnapshot w.result.ModuleSnapshotPath = moduleSnapshotPath w.briefingMetadata = briefing.BuildMetadata(briefingBuildContext(w.req.Config, w.req.Resolved, w.reportFacts.Collected)) w.metadata = state.BuildPromptMetadataFromBriefingMetadata(w.req.Resolved, w.briefingMetadata, state.ArtifactPaths{ ModuleSnapshot: moduleSnapshotPath, Metadata: paths.Metadata, }) w.result.Metadata = w.metadata dataPackage, err := promptinput.Build(promptinput.BuildRequest{ Metadata: promptMetadata(w.metadata), Modules: w.moduleSnapshot, }) if err != nil { return w.reportError("build data package", err) } w.dataPackageBytes, err = promptinput.MarshalYAML(dataPackage) if err != nil { return err } dataPackagePath, err := w.store.SaveDataPackageBytes(w.ctx, w.req.Resolved, w.dataPackageBytes) if err != nil { return err } w.metadata.DataPackagePath = dataPackagePath w.result.DataPackage = dataPackage w.result.DataPackagePath = dataPackagePath w.result.Metadata = w.metadata w.handler, err = generatedtext.LookupDefinition(w.req.Resolved.Definition) if err != nil { return w.reportError("lookup generated text catalog", err) } w.debugRef = promptdebug.PromptDebugRef{ ReportID: w.req.Resolved.Definition.ID, ValidDate: w.req.Resolved.ValidPeriod.Start.Format("2006-01-02"), RunID: w.metadata.RunID, } return nil } func (w *promptReportWorkflow) executePrompt() (*promptexec.Execution, error) { return w.req.Executor.Execute(w.ctx, promptexec.ExecuteRequest{ PromptID: w.req.Inspection.PromptID, PromptVersion: w.req.Inspection.PromptVersion, ProfileID: w.req.Inspection.ProfileID, DataPackage: w.dataPackageBytes, DataPackagePath: w.result.DataPackagePath, CaptureDebug: w.req.DebugWriter.Enabled(), }, w.persistPreparation) } func (w *promptReportWorkflow) persistPreparation(preparation promptexec.Preparation, debug *promptexec.PreparationDebug) error { artifact := state.PromptPreparationArtifact{ SchemaVersion: state.PromptPreparationSchemaVersion, Status: state.PromptPreparationSucceeded, ReportID: w.req.Resolved.Definition.ID, RunID: w.metadata.RunID, PromptID: w.req.Inspection.PromptID, PromptVersion: w.req.Inspection.PromptVersion, DataPackagePath: w.result.DataPackagePath, Preparation: &preparation, StartedAt: preparation.StartedAt, EndedAt: preparation.EndedAt, Duration: preparation.Duration, } path, err := w.store.SavePromptPreparation(w.ctx, w.req.Resolved, artifact) if err != nil { w.callbackFailed = true return err } w.prepared = true w.result.PreparationPath = path w.metadata.PreparationPath = path w.result.Metadata = w.metadata debugPath, err := w.req.DebugWriter.WritePreparation(w.debugRef, preparation, debug) if err != nil { w.callbackFailed = true return promptDebugWriteError(err) } if debugPath != "" { w.result.LLMDebugPath = debugPath } if err := w.saveMetadata(); err != nil { w.callbackFailed = true return err } return nil } func (w *promptReportWorkflow) handleExecutionFailure(executeErr error) error { if w.callbackFailed { return executeErr } executeErr = classifiedPromptError("prompt execution failed", executeErr) if !w.prepared { if err := w.persistPreparationFailure(executeErr); err != nil { return err } return w.reportError("prepare prompt", executeErr) } if promptexec.CategoryOf(executeErr) != "" { if err := w.persistOperationalExecutionFailure(executeErr); err != nil { return err } } return w.reportError("execute prompt", executeErr) } func (w *promptReportWorkflow) persistPreparationFailure(executeErr error) error { startedAt, endedAt := time.Now(), time.Now() artifact := state.PromptPreparationArtifact{ SchemaVersion: state.PromptPreparationSchemaVersion, Status: state.PromptPreparationFailed, ReportID: w.req.Resolved.Definition.ID, RunID: w.metadata.RunID, PromptID: w.req.Inspection.PromptID, PromptVersion: w.req.Inspection.PromptVersion, DataPackagePath: w.result.DataPackagePath, StartedAt: startedAt, EndedAt: endedAt, Error: state.NewPromptArtifactError(executeErr), } path, err := w.store.SavePromptPreparation(w.ctx, w.req.Resolved, artifact) if err != nil { return err } w.result.PreparationPath = path w.metadata.PreparationPath = path w.result.Metadata = w.metadata return w.saveMetadata() } func (w *promptReportWorkflow) persistOperationalExecutionFailure(executeErr error) error { artifact := failedPromptExecutionArtifact(w.req.Resolved, w.metadata, w.req.Inspection, executeErr) path, err := w.store.SavePromptExecution(w.ctx, w.req.Resolved, artifact) if err != nil { return err } w.result.ExecutionPath = path w.metadata.ExecutionPath = path w.result.Metadata = w.metadata return w.saveMetadata() } func (w *promptReportWorkflow) persistExecutionDebug(execution promptexec.Execution) error { debugPath, err := w.req.DebugWriter.WriteExecution(w.debugRef, execution) if err != nil { return w.reportError("write prompt debug", promptDebugWriteError(err)) } if debugPath != "" { w.result.LLMDebugPath = debugPath } return nil } func (w *promptReportWorkflow) persistCompletedExecution(execution promptexec.Execution) error { rawPath, err := w.store.SaveGeneratedTextRaw(w.ctx, w.req.Resolved, execution.RawOutput) if err != nil { return err } w.result.GeneratedTextRawPath = rawPath w.metadata.GeneratedTextRawPath = rawPath w.result.Metadata = w.metadata w.executionArtifact = state.PromptExecutionArtifact{ SchemaVersion: state.PromptExecutionSchemaVersion, ReportID: w.req.Resolved.Definition.ID, RunID: w.metadata.RunID, PromptID: w.req.Inspection.PromptID, PromptVersion: w.req.Inspection.PromptVersion, Provenance: ptr(state.PromptExecutionProvenanceFrom(execution)), Validation: &execution.Validation, Paths: state.PromptExecutionPaths{RawOutputPath: rawPath}, StartedAt: execution.StartedAt, EndedAt: execution.EndedAt, Duration: execution.Duration, } if execution.Validation.Status == promptexec.ValidationPassed { w.executionArtifact.Status = state.PromptExecutionSucceeded } else { w.executionArtifact.Status = state.PromptExecutionValidationRejected } executionPath, err := w.store.SavePromptExecution(w.ctx, w.req.Resolved, w.executionArtifact) if err != nil { return err } w.result.ExecutionPath = executionPath w.metadata.ExecutionPath = executionPath w.result.Metadata = w.metadata return w.saveMetadata() } func (w *promptReportWorkflow) persistGeneratedContent(raw []byte) ([]byte, error) { generatedText, normalized, err := w.handler.Validate(raw) if err != nil { return nil, w.reportError("validate generated text", err) } generatedTextPath, err := w.store.SaveGeneratedText(w.ctx, w.req.Resolved, normalized) if err != nil { return nil, err } w.result.GeneratedTextPath = generatedTextPath w.metadata.GeneratedTextPath = generatedTextPath w.result.Metadata = w.metadata if err := w.persistReachedPathAndMetadata(func(paths *state.PromptExecutionPaths) { paths.GeneratedTextPath = generatedTextPath }); err != nil { return nil, err } renderContext, err := w.handler.BuildRenderContext(w.briefingMetadata, w.moduleSnapshot, w.reportFacts.Collected, w.reportFacts.Derived, generatedText) if err != nil { return nil, w.reportError("build render context", err) } renderContextPath, err := w.store.SaveRenderContext(w.ctx, w.req.Resolved, renderContext) if err != nil { return nil, err } w.result.RenderContextPath = renderContextPath w.metadata.RenderContextPath = renderContextPath w.result.Metadata = w.metadata if err := w.persistReachedPathAndMetadata(func(paths *state.PromptExecutionPaths) { paths.RenderContextPath = renderContextPath }); err != nil { return nil, err } rendered, err := w.handler.Render(renderContext) if err != nil { return nil, w.reportError("render template", err) } return rendered, nil } func (w *promptReportWorkflow) finalizeReport(rendered []byte) (*ReportResult, error) { reportPath, err := w.store.PrepareRenderedReport(w.ctx, w.req.Resolved) if err != nil { return w.result, err } if err := fileutil.WriteFileAtomic(reportPath, rendered); err != nil { return w.result, err } w.result.ReportPath = reportPath w.metadata.RenderedReportPath = reportPath w.result.Metadata = w.metadata if err := w.persistReachedPath(func(paths *state.PromptExecutionPaths) { paths.RenderedReportPath = reportPath }); err != nil { return w.result, err } finalized, err := finalizeRenderedReport(w.ctx, finalizeRenderedReportRequest{ Config: w.req.Config, Store: w.store, Resolved: w.req.Resolved, Metadata: w.metadata, MetadataPath: w.result.MetadataPath, ExecutionArtifact: &w.executionArtifact, RenderedReportPath: reportPath, OutputPath: w.req.OutputPath, Notifier: w.req.Notifier, noNotify: w.req.noNotify, }) w.result.OutputPath = finalized.OutputPath w.result.Metadata, w.result.MetadataPath, w.result.Notification = finalized.Metadata, finalized.MetadataPath, finalized.Notification return w.result, err } func (w *promptReportWorkflow) persistReachedPath(update func(*state.PromptExecutionPaths)) error { return persistReachedPromptPath(w.ctx, w.store, w.req.Resolved, &w.executionArtifact, update) } func (w *promptReportWorkflow) persistReachedPathAndMetadata(update func(*state.PromptExecutionPaths)) error { if err := w.persistReachedPath(update); err != nil { return err } return w.saveMetadata() } func (w *promptReportWorkflow) saveMetadata() error { path, err := w.store.SaveMetadata(w.ctx, w.metadata) if err != nil { return err } w.result.Metadata = w.metadata w.result.MetadataPath = path return nil } func (w *promptReportWorkflow) reportError(operation string, err error) error { return generatedReportError(w.req.Resolved, w.metadata.RunID, operation, err) } func persistReachedPromptPath( ctx context.Context, store state.Store, resolved report.Resolved, artifact *state.PromptExecutionArtifact, update func(*state.PromptExecutionPaths), ) error { update(&artifact.Paths) _, err := store.SavePromptExecution(ctx, resolved, *artifact) return err } func failedPromptExecutionArtifact(resolved report.Resolved, metadata state.Metadata, inspection PromptInspectionResult, err error) state.PromptExecutionArtifact { now := time.Now() return state.PromptExecutionArtifact{ SchemaVersion: state.PromptExecutionSchemaVersion, Status: state.PromptExecutionFailed, ReportID: resolved.Definition.ID, RunID: metadata.RunID, PromptID: inspection.PromptID, PromptVersion: inspection.PromptVersion, StartedAt: now, EndedAt: now, Error: state.NewPromptArtifactError(err), } } func classifiedPromptError(operation string, err error) error { if promptexec.CategoryOf(err) != "" { return err } return promptexec.NewError(promptexec.Generation, operation, err) } func promptDebugWriteError(err error) error { return promptexec.NewError(promptexec.InvalidConfiguration, "write requested prompt debug artifact", err) } func ptr[T any](value T) *T { return &value }