Files
weatherreporter/internal/app/prompt_generate.go

414 lines
15 KiB
Go

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, ManagedReportPath: reportPath, OutputPath: w.req.OutputPath,
Notifier: w.req.Notifier, noNotify: w.req.noNotify,
})
w.result.OutputPath, w.result.NotificationPath = finalized.OutputPath, finalized.NotificationPath
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 }