350 lines
14 KiB
Go
350 lines
14 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/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
|
|
}
|
|
|
|
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{ReportPath: paths.RenderedReport}
|
|
|
|
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,
|
|
DataPackage: paths.DataPackage,
|
|
Preparation: paths.Preparation,
|
|
Execution: paths.Execution,
|
|
RenderedReport: paths.RenderedReport,
|
|
GeneratedTextRaw: paths.GeneratedTextRaw,
|
|
GeneratedText: paths.GeneratedText,
|
|
RenderContext: paths.RenderContext,
|
|
})
|
|
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
|
|
metadataPath, saveErr := store.SaveMetadata(ctx, metadata)
|
|
if saveErr != nil {
|
|
return result, saveErr
|
|
}
|
|
result.PreparationPath, result.Metadata, result.MetadataPath = path, metadata, 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
|
|
metadataPath, saveErr := store.SaveMetadata(ctx, metadata)
|
|
if saveErr != nil {
|
|
return result, saveErr
|
|
}
|
|
result.ExecutionPath, result.Metadata, result.MetadataPath = path, metadata, 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
|
|
metadataPath, saveErr := store.SaveMetadata(ctx, metadata)
|
|
if saveErr != nil {
|
|
return result, saveErr
|
|
}
|
|
result.ExecutionPath, result.Metadata, result.MetadataPath = executionPath, metadata, 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
|
|
metadataPath, saveErr := store.SaveMetadata(ctx, metadata)
|
|
if saveErr != nil {
|
|
return result, saveErr
|
|
}
|
|
result.ExecutionPath, result.Metadata, result.MetadataPath = executionPath, metadata, 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
|
|
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
|
|
metadata.GeneratedTextRawPath = rawPath
|
|
metadataPath, err := store.SaveMetadata(ctx, metadata)
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
result.ExecutionPath, result.Metadata, result.MetadataPath = executionPath, metadata, 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
|
|
metadataPath, err = store.SaveMetadata(ctx, metadata)
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
result.GeneratedTextPath, result.Metadata, result.MetadataPath = generatedTextPath, metadata, 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
|
|
metadataPath, err = store.SaveMetadata(ctx, metadata)
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
result.RenderContextPath, result.Metadata, result.MetadataPath = renderContextPath, metadata, 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
|
|
finalized, err := finalizeRenderedReport(ctx, finalizeRenderedReportRequest{
|
|
Config: req.Config, Store: store, Resolved: req.Resolved, Metadata: metadata,
|
|
ManagedReportPath: reportPath, OutputPath: req.OutputPath, Notifier: req.Notifier,
|
|
})
|
|
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 }
|