package app import ( "context" "fmt" "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/weatherdata" ) type promptReportRequest struct { GenerateRequest Resolved report.Resolved Collection collect.Result Inspection PromptInspectionResult DebugWriter *promptdebug.PromptDebugWriter Result *ReportResult noNotify bool } type promptReportWorkflow struct { ctx context.Context req promptReportRequest result *ReportResult briefingMetadata briefing.Metadata reportFacts ReportFacts moduleSnapshot module.Snapshot dataPackage []byte handler generatedtext.Handler debugRef promptdebug.PromptDebugRef callbackFailed 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, err := workflow.executePrompt() if err != nil { if workflow.callbackFailed { return workflow.result, err } return workflow.result, workflow.reportError("execute prompt", classifiedPromptError("prompt execution failed", err)) } if execution == nil { return workflow.result, workflow.reportError("execute prompt", promptexec.NewError(promptexec.Generation, "prompt executor returned no execution", nil)) } workflow.result.ValidationStatus = execution.Validation.Status if err := workflow.writeExecutionDebug(*execution); err != nil { return workflow.result, err } if execution.Validation.Status != promptexec.ValidationPassed && execution.Validation.Status != promptexec.ValidationFailed { return workflow.result, workflow.reportError("validate prompt execution", promptexec.NewError(promptexec.OperationalValidation, "prompt execution did not complete validation", nil)) } if execution.Validation.Status == promptexec.ValidationFailed { return workflow.result, workflow.reportError("validate prompt execution", promptexec.NewError(promptexec.ValidationRejected, "prompt output did not satisfy its schema", nil)) } return workflow.renderAndPublish(execution.RawOutput) } func newPromptReportWorkflow(ctx context.Context, req promptReportRequest) (*promptReportWorkflow, error) { if req.Collection.Bundle == nil { return nil, fmt.Errorf("collected weather bundle is required") } result := req.Result if result == nil { result = initialReportResult(req.GenerateRequest, req.Resolved, req.Inspection) } return &promptReportWorkflow{ ctx: ctx, req: req, result: result, }, nil } func initialReportResult(req GenerateRequest, resolved report.Resolved, inspection PromptInspectionResult) *ReportResult { metadata := resolved.Metadata() return &ReportResult{ ReportID: resolved.Definition.ID, ReportName: resolved.Definition.Name, PromptID: resolved.Definition.PromptID, PromptVersion: resolved.Definition.PromptVersion, RunID: metadata.RunID, GeneratedAt: metadata.GeneratedAt, Timezone: req.Config.WeatherAPI.Timezone, ValidPeriod: metadata.ValidPeriod, ProfileID: inspection.ProfileID, BackendID: inspection.BackendID, ModelName: inspection.ModelName, } } func (w *promptReportWorkflow) buildInputs() error { var err error w.reportFacts, err = BuildReportFacts(ModuleSnapshotRequest{Config: w.req.Config, Resolved: w.req.Resolved}, w.req.Collection.Bundle) if err != nil { return w.reportError("build report facts", err) } w.moduleSnapshot, err = BuildModuleSnapshotFromFacts(ModuleSnapshotRequest{Config: w.req.Config, Resolved: w.req.Resolved}, w.reportFacts) if err != nil { return w.reportError("build module snapshot", err) } w.briefingMetadata = briefing.BuildMetadata(briefingBuildContext(w.req.Config, w.req.Resolved, w.reportFacts.Collected)) w.result.SourceWarnings = append([]weatherdata.SourceWarning(nil), w.briefingMetadata.SourceWarnings...) dataPackage, err := promptinput.Build(promptinput.BuildRequest{Metadata: promptMetadata(w.briefingMetadata), Modules: w.moduleSnapshot}) if err != nil { return w.reportError("build data package", err) } w.dataPackage, err = promptinput.MarshalYAML(dataPackage) if err != nil { return w.reportError("marshal data package", err) } 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.result.ReportID, ValidDate: w.req.Resolved.ValidPeriod.Start.Format("2006-01-02"), RunID: w.result.RunID} return nil } func (w *promptReportWorkflow) executePrompt() (*promptexec.Execution, error) { captureDebug := w.req.DebugWriter != nil && w.req.DebugWriter.Enabled() 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.dataPackage, CaptureDebug: captureDebug}, w.writePreparationDebug) } func (w *promptReportWorkflow) writePreparationDebug(preparation promptexec.Preparation, debug *promptexec.PreparationDebug) error { w.result.ProfileID, w.result.BackendID, w.result.ModelName = preparation.ProfileID, preparation.BackendID, preparation.ModelName if w.req.DebugWriter == nil { return nil } path, err := w.req.DebugWriter.WritePreparation(w.debugRef, preparation, debug) if err != nil { w.callbackFailed = true return promptDebugWriteError(err) } w.result.LLMDebugPath = path return nil } func (w *promptReportWorkflow) writeExecutionDebug(execution promptexec.Execution) error { if w.req.DebugWriter == nil { return nil } path, err := w.req.DebugWriter.WriteExecution(w.debugRef, execution) if err != nil { return w.reportError("write prompt debug", promptDebugWriteError(err)) } if path != "" { w.result.LLMDebugPath = path } return nil } func (w *promptReportWorkflow) renderAndPublish(raw []byte) (*ReportResult, error) { generatedText, _, err := w.handler.Validate(raw) if err != nil { return w.result, w.reportError("validate generated text", err) } renderContext, err := w.handler.BuildRenderContext(w.briefingMetadata, w.moduleSnapshot, w.reportFacts.Collected, w.reportFacts.Derived, generatedText) if err != nil { return w.result, w.reportError("build render context", err) } rendered, err := w.handler.Render(renderContext) if err != nil { return w.result, w.reportError("render template", err) } if err := fileutil.WriteFileAtomic(w.req.OutputPath, rendered); err != nil { return w.result, err } w.result.OutputPath = w.req.OutputPath if w.req.noNotify { return w.result, nil } notification, err := notifyReport(w.ctx, w.req.Config, w.req.Resolved, w.result.OutputPath, w.result.RunID, w.result.GeneratedAt, w.req.Notifier) w.result.Notification = notification if err != nil { return w.result, err } return w.result, nil } func (w *promptReportWorkflow) reportError(operation string, err error) error { return generatedReportError(w.req.Resolved, w.result.RunID, operation, 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) }