Enhance debug output to include response content files and update related metadata handling
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
package pipeline
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path"
|
||||
@@ -22,14 +24,16 @@ import (
|
||||
type DebugRecorder interface {
|
||||
Enabled() bool
|
||||
WriteJSON(name string, payload any) error
|
||||
WriteBytes(name string, data []byte) error
|
||||
}
|
||||
|
||||
type noopDebugRecorder struct{}
|
||||
|
||||
func NoopDebugRecorder() DebugRecorder { return noopDebugRecorder{} }
|
||||
|
||||
func (noopDebugRecorder) Enabled() bool { return false }
|
||||
func (noopDebugRecorder) WriteJSON(string, any) error { return nil }
|
||||
func (noopDebugRecorder) Enabled() bool { return false }
|
||||
func (noopDebugRecorder) WriteJSON(string, any) error { return nil }
|
||||
func (noopDebugRecorder) WriteBytes(string, []byte) error { return nil }
|
||||
func debugPathComponent(value string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
@@ -171,20 +175,22 @@ type debugLLMPromptArtifact struct {
|
||||
}
|
||||
|
||||
type debugLLMResponseArtifact struct {
|
||||
CallID string `json:"call_id"`
|
||||
Response *contracts.LLMDebugResponse `json:"response,omitempty"`
|
||||
Fallback *debugStructuredCompletionResponse `json:"fallback,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
CallID string `json:"call_id"`
|
||||
Response *contracts.LLMDebugResponse `json:"response,omitempty"`
|
||||
Fallback *debugStructuredCompletionResponse `json:"fallback,omitempty"`
|
||||
ContentPath string `json:"content_path,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type debugLLMCallReference struct {
|
||||
CallID string `json:"call_id"`
|
||||
PromptPath string `json:"prompt_path,omitempty"`
|
||||
ResponsePath string `json:"response_path"`
|
||||
PromptID string `json:"prompt_id,omitempty"`
|
||||
ProfileID string `json:"profile_id,omitempty"`
|
||||
Model string `json:"model,omitempty"`
|
||||
Error bool `json:"error,omitempty"`
|
||||
CallID string `json:"call_id"`
|
||||
PromptPath string `json:"prompt_path,omitempty"`
|
||||
ResponsePath string `json:"response_path"`
|
||||
ResponseContentPath string `json:"response_content_path,omitempty"`
|
||||
PromptID string `json:"prompt_id,omitempty"`
|
||||
ProfileID string `json:"profile_id,omitempty"`
|
||||
Model string `json:"model,omitempty"`
|
||||
Error bool `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type debugValidationRequest struct {
|
||||
@@ -274,6 +280,10 @@ func (client *debugLLMClient) CompleteStructured(ctx context.Context, req contra
|
||||
}))
|
||||
}
|
||||
responsePath := path.Join(scopePrefix, "response-"+callID+".json")
|
||||
responseMaterial := debugResponseMaterial(response)
|
||||
fallbackMaterial := debugCompletionFallback(response)
|
||||
responseContentPath, responseForArtifact, fallbackForArtifact, contentErr := writeDebugResponseContent(client.recorder, scopePrefix, callID, responseMaterial, fallbackMaterial)
|
||||
writeErr = errors.Join(writeErr, contentErr)
|
||||
writeErr = errors.Join(writeErr, writeDebugTimed(client.recorder, responsePath, debugTimedEnvelope{
|
||||
Stage: req.StageName,
|
||||
ModuleKey: req.StageName,
|
||||
@@ -281,21 +291,23 @@ func (client *debugLLMClient) CompleteStructured(ctx context.Context, req contra
|
||||
CompletedAt: completed,
|
||||
DurationMS: completed.Sub(started).Milliseconds(),
|
||||
Payload: debugLLMResponseArtifact{
|
||||
CallID: callID,
|
||||
Response: debugResponseMaterial(response),
|
||||
Fallback: debugCompletionFallback(response),
|
||||
Error: errorText,
|
||||
CallID: callID,
|
||||
Response: responseForArtifact,
|
||||
Fallback: fallbackForArtifact,
|
||||
ContentPath: responseContentPath,
|
||||
Error: errorText,
|
||||
},
|
||||
Error: errorText,
|
||||
}))
|
||||
callRef := debugLLMCallReference{
|
||||
CallID: callID,
|
||||
PromptPath: promptPath,
|
||||
ResponsePath: responsePath,
|
||||
PromptID: req.PromptID,
|
||||
ProfileID: debugFirstNonEmptyString(response.ProfileID, req.ProfileID),
|
||||
Model: debugFirstNonEmptyString(response.Model, debugResponseModel(response)),
|
||||
Error: err != nil,
|
||||
CallID: callID,
|
||||
PromptPath: promptPath,
|
||||
ResponsePath: responsePath,
|
||||
ResponseContentPath: responseContentPath,
|
||||
PromptID: req.PromptID,
|
||||
ProfileID: debugFirstNonEmptyString(response.ProfileID, req.ProfileID),
|
||||
Model: debugFirstNonEmptyString(response.Model, debugResponseModel(response)),
|
||||
Error: err != nil,
|
||||
}
|
||||
if scope := debugLLMScopeFromContext(ctx); scope != nil {
|
||||
scope.record(callRef)
|
||||
@@ -593,6 +605,60 @@ func debugCompletionFallback(response contracts.StructuredCompletionResponse) *d
|
||||
return &fallback
|
||||
}
|
||||
|
||||
func writeDebugResponseContent(recorder DebugRecorder, scopePrefix string, callID string, response *contracts.LLMDebugResponse, fallback *debugStructuredCompletionResponse) (string, *contracts.LLMDebugResponse, *debugStructuredCompletionResponse, error) {
|
||||
responseCopy := cloneDebugResponseWithoutContent(response)
|
||||
fallbackCopy := cloneDebugFallbackWithoutContent(fallback)
|
||||
content := ""
|
||||
if response != nil {
|
||||
content = response.Content
|
||||
}
|
||||
if content == "" && fallback != nil {
|
||||
content = fallback.Content
|
||||
}
|
||||
if content == "" {
|
||||
return "", responseCopy, fallbackCopy, nil
|
||||
}
|
||||
|
||||
contentPath, data := debugResponseContentFile(scopePrefix, callID, content)
|
||||
if recorder == nil || !recorder.Enabled() {
|
||||
return contentPath, responseCopy, fallbackCopy, nil
|
||||
}
|
||||
if err := recorder.WriteBytes(contentPath, data); err != nil {
|
||||
return contentPath, responseCopy, fallbackCopy, err
|
||||
}
|
||||
return contentPath, responseCopy, fallbackCopy, nil
|
||||
}
|
||||
|
||||
func cloneDebugResponseWithoutContent(response *contracts.LLMDebugResponse) *contracts.LLMDebugResponse {
|
||||
if response == nil {
|
||||
return nil
|
||||
}
|
||||
clone := *response
|
||||
clone.Content = ""
|
||||
return &clone
|
||||
}
|
||||
|
||||
func cloneDebugFallbackWithoutContent(fallback *debugStructuredCompletionResponse) *debugStructuredCompletionResponse {
|
||||
if fallback == nil {
|
||||
return nil
|
||||
}
|
||||
clone := *fallback
|
||||
clone.Content = ""
|
||||
return &clone
|
||||
}
|
||||
|
||||
func debugResponseContentFile(scopePrefix string, callID string, content string) (string, []byte) {
|
||||
raw := []byte(content)
|
||||
if json.Valid(raw) {
|
||||
var formatted bytes.Buffer
|
||||
if err := json.Indent(&formatted, raw, "", " "); err == nil {
|
||||
formatted.WriteByte('\n')
|
||||
return path.Join(scopePrefix, "response-content-"+callID+".json"), formatted.Bytes()
|
||||
}
|
||||
}
|
||||
return path.Join(scopePrefix, "response-content-"+callID+".txt"), raw
|
||||
}
|
||||
|
||||
func debugResponseMaterial(response contracts.StructuredCompletionResponse) *contracts.LLMDebugResponse {
|
||||
if response.Debug == nil {
|
||||
return nil
|
||||
|
||||
Reference in New Issue
Block a user