Update the debug workflow to provide raw LLM output
This commit is contained in:
@@ -5,10 +5,12 @@ import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
@@ -56,15 +58,16 @@ func debugPathComponent(value string) string {
|
||||
}
|
||||
|
||||
type debugTimedEnvelope struct {
|
||||
Stage string `json:"stage,omitempty"`
|
||||
LaneID string `json:"lane_id,omitempty"`
|
||||
ModuleKey string `json:"module_key,omitempty"`
|
||||
Attempt int `json:"attempt,omitempty"`
|
||||
StartedAt time.Time `json:"started_at"`
|
||||
CompletedAt time.Time `json:"completed_at"`
|
||||
DurationMS int64 `json:"duration_ms"`
|
||||
Payload any `json:"payload,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Stage string `json:"stage,omitempty"`
|
||||
LaneID string `json:"lane_id,omitempty"`
|
||||
ModuleKey string `json:"module_key,omitempty"`
|
||||
Attempt int `json:"attempt,omitempty"`
|
||||
StartedAt time.Time `json:"started_at"`
|
||||
CompletedAt time.Time `json:"completed_at"`
|
||||
DurationMS int64 `json:"duration_ms"`
|
||||
Payload any `json:"payload,omitempty"`
|
||||
LLMCalls []debugLLMCallReference `json:"llm_calls,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type debugBinaryEnvelope struct {
|
||||
@@ -168,6 +171,15 @@ type debugStructuredLLMCall struct {
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type debugLLMCallReference struct {
|
||||
CallID string `json:"call_id"`
|
||||
CanonicalPath string `json:"canonical_path"`
|
||||
ScopedPath string `json:"scoped_path,omitempty"`
|
||||
PromptID string `json:"prompt_id,omitempty"`
|
||||
ProfileID string `json:"profile_id,omitempty"`
|
||||
Error bool `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type debugValidationRequest struct {
|
||||
Stage string `json:"stage"`
|
||||
LaneID string `json:"lane_id,omitempty"`
|
||||
@@ -197,9 +209,19 @@ type debugValidationCall struct {
|
||||
type debugLLMClient struct {
|
||||
inner contracts.StructuredLLMClient
|
||||
recorder DebugRecorder
|
||||
mu sync.Mutex
|
||||
counter int
|
||||
}
|
||||
|
||||
type debugLLMScope struct {
|
||||
prefix string
|
||||
parent *debugLLMScope
|
||||
mu sync.Mutex
|
||||
calls []debugLLMCallReference
|
||||
}
|
||||
|
||||
type debugLLMScopeContextKey struct{}
|
||||
|
||||
func wrapDebugLLMClient(client contracts.StructuredLLMClient, recorder DebugRecorder) contracts.StructuredLLMClient {
|
||||
if client == nil || recorder == nil || !recorder.Enabled() {
|
||||
return client
|
||||
@@ -208,7 +230,11 @@ func wrapDebugLLMClient(client contracts.StructuredLLMClient, recorder DebugReco
|
||||
}
|
||||
|
||||
func (client *debugLLMClient) CompleteStructured(ctx context.Context, req contracts.StructuredCompletionRequest, out any) (contracts.StructuredCompletionResponse, error) {
|
||||
client.mu.Lock()
|
||||
client.counter++
|
||||
callID := fmt.Sprintf("call-%04d", client.counter)
|
||||
client.mu.Unlock()
|
||||
|
||||
started := time.Now().UTC()
|
||||
response, err := client.inner.CompleteStructured(ctx, req, out)
|
||||
completed := time.Now().UTC()
|
||||
@@ -219,7 +245,7 @@ func (client *debugLLMClient) CompleteStructured(ctx context.Context, req contra
|
||||
if err != nil {
|
||||
payload.Error = err.Error()
|
||||
}
|
||||
writeErr := writeDebugTimed(client.recorder, path.Join("llm", fmt.Sprintf("call-%04d.json", client.counter)), debugTimedEnvelope{
|
||||
envelope := debugTimedEnvelope{
|
||||
Stage: req.StageName,
|
||||
ModuleKey: req.StageName,
|
||||
StartedAt: started,
|
||||
@@ -227,7 +253,25 @@ func (client *debugLLMClient) CompleteStructured(ctx context.Context, req contra
|
||||
DurationMS: completed.Sub(started).Milliseconds(),
|
||||
Payload: payload,
|
||||
Error: payload.Error,
|
||||
})
|
||||
}
|
||||
canonicalPath := path.Join("llm", callID+".json")
|
||||
writeErr := writeDebugTimed(client.recorder, canonicalPath, envelope)
|
||||
callRef := debugLLMCallReference{
|
||||
CallID: callID,
|
||||
CanonicalPath: canonicalPath,
|
||||
PromptID: req.PromptID,
|
||||
ProfileID: debugFirstNonEmptyString(response.ProfileID, req.ProfileID),
|
||||
Error: err != nil,
|
||||
}
|
||||
if scope := debugLLMScopeFromContext(ctx); scope != nil {
|
||||
scopedPath := path.Join(scope.prefix, "llm-"+callID+".json")
|
||||
callRef.ScopedPath = scopedPath
|
||||
scopedWriteErr := writeDebugTimed(client.recorder, scopedPath, envelope)
|
||||
if scopedWriteErr != nil {
|
||||
writeErr = errors.Join(writeErr, scopedWriteErr)
|
||||
}
|
||||
scope.record(callRef)
|
||||
}
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
@@ -237,6 +281,73 @@ func (client *debugLLMClient) CompleteStructured(ctx context.Context, req contra
|
||||
return response, err
|
||||
}
|
||||
|
||||
func withDebugLLMScope(ctx context.Context, prefix string) (context.Context, *debugLLMScope) {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
prefix = cleanDebugPath(prefix)
|
||||
scope := &debugLLMScope{
|
||||
prefix: prefix,
|
||||
parent: debugLLMScopeFromContext(ctx),
|
||||
}
|
||||
return context.WithValue(ctx, debugLLMScopeContextKey{}, scope), scope
|
||||
}
|
||||
|
||||
func debugLLMScopeFromContext(ctx context.Context) *debugLLMScope {
|
||||
if ctx == nil {
|
||||
return nil
|
||||
}
|
||||
scope, _ := ctx.Value(debugLLMScopeContextKey{}).(*debugLLMScope)
|
||||
return scope
|
||||
}
|
||||
|
||||
func (scope *debugLLMScope) record(ref debugLLMCallReference) {
|
||||
if scope == nil {
|
||||
return
|
||||
}
|
||||
scope.mu.Lock()
|
||||
scope.calls = append(scope.calls, ref)
|
||||
scope.mu.Unlock()
|
||||
if scope.parent != nil {
|
||||
scope.parent.record(ref)
|
||||
}
|
||||
}
|
||||
|
||||
func (scope *debugLLMScope) references() []debugLLMCallReference {
|
||||
if scope == nil {
|
||||
return nil
|
||||
}
|
||||
scope.mu.Lock()
|
||||
defer scope.mu.Unlock()
|
||||
if len(scope.calls) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]debugLLMCallReference, len(scope.calls))
|
||||
copy(out, scope.calls)
|
||||
return out
|
||||
}
|
||||
|
||||
func cleanDebugPath(value string) string {
|
||||
parts := strings.Split(path.Clean(strings.TrimSpace(value)), "/")
|
||||
out := make([]string, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
out = append(out, debugPathComponent(part))
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return "_"
|
||||
}
|
||||
return path.Join(out...)
|
||||
}
|
||||
|
||||
func debugFirstNonEmptyString(values ...string) string {
|
||||
for _, value := range values {
|
||||
if strings.TrimSpace(value) != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (client *debugLLMClient) LLMProfileManifests() []artifacts.LLMProfileManifest {
|
||||
provider, ok := client.inner.(contracts.LLMProfileManifestProvider)
|
||||
if !ok {
|
||||
@@ -261,6 +372,13 @@ func writeDebugTimed(recorder DebugRecorder, name string, envelope debugTimedEnv
|
||||
return recorder.WriteJSON(name, envelope)
|
||||
}
|
||||
|
||||
func debugEnvelopeWithLLMCalls(envelope debugTimedEnvelope, scope *debugLLMScope) debugTimedEnvelope {
|
||||
if scope != nil {
|
||||
envelope.LLMCalls = scope.references()
|
||||
}
|
||||
return envelope
|
||||
}
|
||||
|
||||
func debugContentEnvelope(content []byte, mediaType string, metadata map[string]any, warnings []contracts.Warning) debugBinaryEnvelope {
|
||||
content = redactSecretBytes(content)
|
||||
return debugBinaryEnvelope{
|
||||
|
||||
Reference in New Issue
Block a user