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/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 *state.PromptDebugWriter noNotify bool } func generatePromptReport(ctx context.Context, req promptReportRequest) (*ReportResult, error) { bundle := req.Collection.Bundle if 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 } } paths, err := store.Paths(req.Resolved) if err != nil { return nil, err } result := &ReportResult{} priorSnapshot, err := store.FindPriorSnapshot(ctx, req.Resolved) if err != nil { return result, err } reportFacts, err := BuildReportFacts(ModuleSnapshotRequest{Config: req.Config, Resolved: req.Resolved}, bundle) if err != nil { return result, generatedReportError(req.Resolved, req.Resolved.Metadata().RunID, "build report facts", err) } moduleSnapshot, err := BuildModuleSnapshotFromFacts(ModuleSnapshotRequest{Config: req.Config, Resolved: req.Resolved}, reportFacts) if err != nil { return result, generatedReportError(req.Resolved, req.Resolved.Metadata().RunID, "build module snapshot", err) } moduleSnapshotPath, err := store.SaveModuleSnapshot(ctx, req.Resolved, moduleSnapshot) if err != nil { return result, err } result.ModuleSnapshot = moduleSnapshot result.ModuleSnapshotPath = moduleSnapshotPath result.PriorSnapshot = priorSnapshot recent, err := recentChanges(ctx, store, priorSnapshot, req.Resolved.Definition.ID, moduleSnapshot, req.Config.RecentChange) if err != nil { return result, err } result.RecentChanges = recent briefingMetadata := briefing.BuildMetadata(briefingBuildContext(req.Config, req.Resolved, reportFacts.Collected)) metadata := state.BuildPromptMetadataFromBriefingMetadata(req.Resolved, briefingMetadata, state.ArtifactPaths{ ModuleSnapshot: moduleSnapshotPath, Metadata: paths.Metadata, }) result.Metadata = metadata dataPackage, err := promptinput.Build(promptinput.BuildRequest{ Metadata: promptMetadata(metadata), Modules: moduleSnapshot, RecentChanges: recent, }) if err != nil { return result, generatedReportError(req.Resolved, metadata.RunID, "build data package", err) } data, err := promptinput.MarshalYAML(dataPackage) if err != nil { return result, err } dataPackagePath, err := store.SaveDataPackageBytes(ctx, req.Resolved, data) if err != nil { return result, err } metadata.DataPackagePath = dataPackagePath result.DataPackage = dataPackage result.DataPackagePath = dataPackagePath result.Metadata = metadata handler, err := generatedtext.LookupDefinition(req.Resolved.Definition) if err != nil { return result, generatedReportError(req.Resolved, metadata.RunID, "lookup generated text catalog", err) } prepared := false callbackFailed := false debugRef := state.PromptDebugRef{ ReportID: req.Resolved.Definition.ID, ValidDate: req.Resolved.ValidPeriod.Start.Format("2006-01-02"), RunID: metadata.RunID, } callback := func(preparation promptexec.Preparation, debug *promptexec.PreparationDebug) error { artifact := state.PromptPreparationArtifact{ SchemaVersion: state.PromptPreparationSchemaVersion, Status: state.PromptPreparationSucceeded, ReportID: req.Resolved.Definition.ID, RunID: metadata.RunID, PromptID: req.Inspection.PromptID, PromptVersion: req.Inspection.PromptVersion, DataPackagePath: dataPackagePath, Preparation: &preparation, StartedAt: preparation.StartedAt, EndedAt: preparation.EndedAt, Duration: preparation.Duration, } path, err := store.SavePromptPreparation(ctx, req.Resolved, artifact) if err != nil { callbackFailed = true return err } prepared = true result.PreparationPath = path metadata.PreparationPath = path result.Metadata = metadata debugPath, err := req.DebugWriter.WritePreparation(debugRef, preparation, debug) if err != nil { callbackFailed = true return promptDebugWriteError(err) } if debugPath != "" { result.LLMDebugPath = debugPath } metadataPath, err := store.SaveMetadata(ctx, metadata) if err != nil { callbackFailed = true return err } result.Metadata = metadata result.MetadataPath = metadataPath return nil } execution, executeErr := req.Executor.Execute(ctx, promptexec.ExecuteRequest{ PromptID: req.Inspection.PromptID, PromptVersion: req.Inspection.PromptVersion, ProfileID: req.Inspection.ProfileID, DataPackage: data, DataPackagePath: dataPackagePath, CaptureDebug: req.DebugWriter.Enabled(), }, callback) if executeErr != nil { if callbackFailed { return result, executeErr } executeErr = classifiedPromptError("prompt execution failed", executeErr) if !prepared { artifact := state.PromptPreparationArtifact{ SchemaVersion: state.PromptPreparationSchemaVersion, Status: state.PromptPreparationFailed, ReportID: req.Resolved.Definition.ID, RunID: metadata.RunID, PromptID: req.Inspection.PromptID, PromptVersion: req.Inspection.PromptVersion, DataPackagePath: dataPackagePath, StartedAt: time.Now(), EndedAt: time.Now(), Error: state.NewPromptArtifactError(executeErr), } path, saveErr := store.SavePromptPreparation(ctx, req.Resolved, artifact) if saveErr != nil { return result, saveErr } metadata.PreparationPath = path result.PreparationPath = path result.Metadata = metadata metadataPath, saveErr := store.SaveMetadata(ctx, metadata) if saveErr != nil { return result, saveErr } result.MetadataPath = metadataPath return result, generatedReportError(req.Resolved, metadata.RunID, "prepare prompt", executeErr) } if promptexec.CategoryOf(executeErr) != "" { artifact := failedPromptExecutionArtifact(req.Resolved, metadata, req.Inspection, executeErr) path, saveErr := store.SavePromptExecution(ctx, req.Resolved, artifact) if saveErr != nil { return result, saveErr } metadata.ExecutionPath = path result.ExecutionPath = path result.Metadata = metadata metadataPath, saveErr := store.SaveMetadata(ctx, metadata) if saveErr != nil { return result, saveErr } result.MetadataPath = metadataPath } return result, generatedReportError(req.Resolved, metadata.RunID, "execute prompt", executeErr) } if execution == nil { err := promptexec.NewError(promptexec.Generation, "prompt executor returned no execution", nil) artifact := failedPromptExecutionArtifact(req.Resolved, metadata, req.Inspection, err) executionPath, saveErr := store.SavePromptExecution(ctx, req.Resolved, artifact) if saveErr != nil { return result, saveErr } metadata.ExecutionPath = executionPath result.ExecutionPath = executionPath result.Metadata = metadata metadataPath, saveErr := store.SaveMetadata(ctx, metadata) if saveErr != nil { return result, saveErr } result.MetadataPath = metadataPath return result, generatedReportError(req.Resolved, metadata.RunID, "execute prompt", err) } debugPath, err := req.DebugWriter.WriteExecution(debugRef, *execution) if err != nil { return result, generatedReportError(req.Resolved, metadata.RunID, "write prompt debug", promptDebugWriteError(err)) } if debugPath != "" { result.LLMDebugPath = debugPath } if execution.Validation.Status != promptexec.ValidationPassed && execution.Validation.Status != promptexec.ValidationFailed { err := promptexec.NewError(promptexec.OperationalValidation, "prompt execution did not complete validation", nil) artifact := failedPromptExecutionArtifact(req.Resolved, metadata, req.Inspection, err) executionPath, saveErr := store.SavePromptExecution(ctx, req.Resolved, artifact) if saveErr != nil { return result, saveErr } metadata.ExecutionPath = executionPath result.ExecutionPath = executionPath result.Metadata = metadata metadataPath, saveErr := store.SaveMetadata(ctx, metadata) if saveErr != nil { return result, saveErr } result.MetadataPath = metadataPath return result, generatedReportError(req.Resolved, metadata.RunID, "validate prompt execution", err) } rawPath, err := store.SaveGeneratedTextRaw(ctx, req.Resolved, execution.RawOutput) if err != nil { return result, err } result.GeneratedTextRawPath = rawPath metadata.GeneratedTextRawPath = rawPath result.Metadata = metadata executionArtifact := state.PromptExecutionArtifact{ SchemaVersion: state.PromptExecutionSchemaVersion, ReportID: req.Resolved.Definition.ID, RunID: metadata.RunID, PromptID: req.Inspection.PromptID, PromptVersion: 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 { executionArtifact.Status = state.PromptExecutionSucceeded } else { executionArtifact.Status = state.PromptExecutionValidationRejected } executionPath, err := store.SavePromptExecution(ctx, req.Resolved, executionArtifact) if err != nil { return result, err } metadata.ExecutionPath = executionPath result.ExecutionPath = executionPath result.Metadata = metadata metadataPath, err := store.SaveMetadata(ctx, metadata) if err != nil { return result, err } result.MetadataPath = metadataPath if execution.Validation.Status == promptexec.ValidationFailed { return result, generatedReportError(req.Resolved, metadata.RunID, "validate prompt execution", promptexec.NewError(promptexec.ValidationRejected, "prompt output did not satisfy its schema", nil)) } generatedText, normalized, err := handler.Validate(execution.RawOutput) if err != nil { return result, generatedReportError(req.Resolved, metadata.RunID, "validate generated text", err) } generatedTextPath, err := store.SaveGeneratedText(ctx, req.Resolved, normalized) if err != nil { return result, err } metadata.GeneratedTextPath = generatedTextPath result.GeneratedTextPath = generatedTextPath result.Metadata = metadata executionArtifact.Paths.GeneratedTextPath = generatedTextPath if _, err := store.SavePromptExecution(ctx, req.Resolved, executionArtifact); err != nil { return result, err } metadataPath, err = store.SaveMetadata(ctx, metadata) if err != nil { return result, err } result.MetadataPath = metadataPath renderContext, err := handler.BuildRenderContext(briefingMetadata, moduleSnapshot, reportFacts.Collected, reportFacts.Derived, generatedText) if err != nil { return result, generatedReportError(req.Resolved, metadata.RunID, "build render context", err) } renderContextPath, err := store.SaveRenderContext(ctx, req.Resolved, renderContext) if err != nil { return result, err } metadata.RenderContextPath = renderContextPath result.RenderContextPath = renderContextPath result.Metadata = metadata executionArtifact.Paths.RenderContextPath = renderContextPath if _, err := store.SavePromptExecution(ctx, req.Resolved, executionArtifact); err != nil { return result, err } metadataPath, err = store.SaveMetadata(ctx, metadata) if err != nil { return result, err } result.MetadataPath = metadataPath rendered, err := handler.Render(renderContext) if err != nil { return result, generatedReportError(req.Resolved, metadata.RunID, "render template", err) } reportPath, err := store.PrepareRenderedReport(ctx, req.Resolved) if err != nil { return result, err } if err := fileutil.WriteFileAtomic(reportPath, rendered); err != nil { return result, err } result.ReportPath = reportPath metadata.RenderedReportPath = reportPath result.Metadata = metadata executionArtifact.Paths.RenderedReportPath = reportPath if _, err := store.SavePromptExecution(ctx, req.Resolved, executionArtifact); err != nil { return result, err } finalized, err := finalizeRenderedReport(ctx, finalizeRenderedReportRequest{ Config: req.Config, Store: store, Resolved: req.Resolved, Metadata: metadata, MetadataPath: result.MetadataPath, ExecutionArtifact: &executionArtifact, ManagedReportPath: reportPath, OutputPath: req.OutputPath, Notifier: req.Notifier, noNotify: req.noNotify, }) result.OutputPath, result.NotificationPath = finalized.OutputPath, finalized.NotificationPath result.Metadata, result.MetadataPath, result.Notification = finalized.Metadata, finalized.MetadataPath, finalized.Notification if err != nil { return result, err } return result, nil } 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 }