215 lines
8.3 KiB
Go
215 lines
8.3 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"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 := publicationContextError(w.ctx); err != nil {
|
|
return w.result, w.reportError("publish report", 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 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)
|
|
}
|