package app import ( "context" "errors" "fmt" "gitea.maximumdirect.net/eric/weatherreporter/internal/collect" "gitea.maximumdirect.net/eric/weatherreporter/internal/fileutil" "gitea.maximumdirect.net/eric/weatherreporter/internal/promptdebug" "gitea.maximumdirect.net/eric/weatherreporter/internal/promptexec" "gitea.maximumdirect.net/eric/weatherreporter/internal/report" ) type promptReportRequest struct { GenerateRequest Resolved report.Resolved Collection collect.Result Inspection PromptInspectionResult DebugWriter *promptdebug.PromptDebugWriter Result *ReportResult noNotify bool } func generatePromptReport(ctx context.Context, req promptReportRequest) (*ReportResult, 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) } prepared, err := prepareReport(prepareReportRequest{Config: req.Config, Resolved: req.Resolved, Collection: req.Collection}) if err != nil { return result, generatedPreparationError(req.Resolved, result.RunID, err) } result.SourceWarnings = prepared.sourceWarningsCopy() debugRef := promptdebug.PromptDebugRef{ReportID: result.ReportID, ValidDate: prepared.resolved.ValidPeriod.Start.Format("2006-01-02"), RunID: result.RunID} outcome, rendered, err := executePreparedProfile(ctx, profileExecutionRequest{ Prepared: prepared, Prompt: req.Inspection, Profile: promptexec.ProfileInspection{ ProfileID: req.Inspection.ProfileID, BackendID: req.Inspection.BackendID, ModelName: req.Inspection.ModelName, }, Executor: req.Executor, DebugWriter: req.DebugWriter, DebugRef: &debugRef, }) result.ProfileID, result.BackendID, result.ModelName = outcome.ProfileID, outcome.BackendID, outcome.ModelName result.ValidationStatus = outcome.ValidationStatus result.LLMDebugPath = outcome.LLMDebugPath if err != nil { return result, generatedProfileExecutionError(req.Resolved, result.RunID, err) } return publishPromptReport(ctx, promptPublicationRequest{ GenerateRequest: req.GenerateRequest, Resolved: req.Resolved, OutputPath: req.OutputPath, Result: result, Markdown: rendered, suppressNotification: req.noNotify, }) } 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, } } type promptPublicationRequest struct { GenerateRequest Resolved report.Resolved OutputPath string Result *ReportResult Markdown []byte suppressNotification bool } func publishPromptReport(ctx context.Context, req promptPublicationRequest) (*ReportResult, error) { if err := publicationContextError(ctx); err != nil { return req.Result, generatedReportError(req.Resolved, req.Result.RunID, "publish report", err) } if err := fileutil.WriteFileAtomic(req.OutputPath, req.Markdown); err != nil { return req.Result, err } req.Result.OutputPath = req.OutputPath if req.suppressNotification { return req.Result, nil } notification, err := notifyReport(ctx, req.Config, req.Resolved, req.Result.OutputPath, req.Result.RunID, req.Result.GeneratedAt, req.Notifier) req.Result.Notification = notification if err != nil { return req.Result, err } return req.Result, nil } func generatedPreparationError(resolved report.Resolved, runID string, err error) error { var preparation *preparationError if errors.As(err, &preparation) { return generatedReportError(resolved, runID, preparation.operation, preparation.err) } return generatedReportError(resolved, runID, "prepare report", err) } func generatedProfileExecutionError(resolved report.Resolved, runID string, err error) error { var execution *profileExecutionError if errors.As(err, &execution) { if execution.callbackFailure { return execution.err } return generatedReportError(resolved, runID, execution.operation, execution.err) } return generatedReportError(resolved, runID, "execute prompt", err) } func classifiedPromptError(operation string, err error) error { if promptexec.CategoryOf(err) != "" { return err } return promptexec.NewError(promptexec.Generation, operation, err) } func publicationContextError(ctx context.Context) error { if err := ctx.Err(); err != nil { if errors.Is(err, context.DeadlineExceeded) { return promptexec.NewError(promptexec.DeadlineExceeded, "context expired before output publication", err) } return promptexec.NewError(promptexec.Canceled, "context canceled before output publication", err) } return nil } func promptDebugWriteError(err error) error { return promptexec.NewError(promptexec.InvalidConfiguration, "write requested prompt debug artifact", err) }