a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
148 lines
4.7 KiB
Go
148 lines
4.7 KiB
Go
package packeval
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// LLM-format-comprehension: a packed context is only useful if the model
|
|
// can actually read it. The same symbols rendered as JSON, GCX1, TOON,
|
|
// or Markdown cost different tokens and read differently; this measures
|
|
// whether an LLM can still answer a grounded question from each format.
|
|
// It is the comprehension half of the packing eval — density without
|
|
// comprehension is a false economy.
|
|
|
|
// Asker runs one prompt against an LLM and returns its raw answer. It is
|
|
// injected so the harness stays provider-agnostic: the CLI wires it to
|
|
// the configured llm.Service; tests pass a deterministic stub. A nil
|
|
// Asker means "no provider" and RunFormatComprehension reports skipped.
|
|
type Asker func(prompt string) (string, error)
|
|
|
|
// FormatRenderer renders a packed context (a list of symbol entries)
|
|
// into a wire format's textual form. Keyed by format name.
|
|
type FormatRenderer func(entries []ContextEntry) string
|
|
|
|
// ContextEntry is one symbol in a packed context used for the
|
|
// comprehension probe: enough to render and to ground a question.
|
|
type ContextEntry struct {
|
|
ID string
|
|
Name string
|
|
FilePath string
|
|
Signature string
|
|
Source string
|
|
}
|
|
|
|
// ComprehensionQuestion is a grounded Q/A whose answer is derivable from
|
|
// the packed context. Correctness is a case-insensitive substring match
|
|
// of any Accept string in the model's answer — robust to phrasing.
|
|
type ComprehensionQuestion struct {
|
|
Question string
|
|
Accept []string
|
|
}
|
|
|
|
// FormatResult is one format's comprehension score.
|
|
type FormatResult struct {
|
|
Format string `json:"format"`
|
|
Asked int `json:"asked"`
|
|
Correct int `json:"correct"`
|
|
Accuracy float64 `json:"accuracy"`
|
|
Tokens int `json:"tokens"`
|
|
Skipped string `json:"skipped,omitempty"`
|
|
}
|
|
|
|
// ComprehensionReport bundles per-format results.
|
|
type ComprehensionReport struct {
|
|
Questions int `json:"questions"`
|
|
Formats []FormatResult `json:"formats"`
|
|
}
|
|
|
|
// RunFormatComprehension renders the packed context in each format and
|
|
// asks the model every question, scoring comprehension per format. A nil
|
|
// or failing asker yields a skipped result per format (never an error),
|
|
// so the probe degrades cleanly when no LLM provider is configured.
|
|
func RunFormatComprehension(
|
|
entries []ContextEntry,
|
|
questions []ComprehensionQuestion,
|
|
renderers map[string]FormatRenderer,
|
|
tokenCount func(string) int,
|
|
ask Asker,
|
|
) ComprehensionReport {
|
|
rep := ComprehensionReport{Questions: len(questions)}
|
|
formats := make([]string, 0, len(renderers))
|
|
for f := range renderers {
|
|
formats = append(formats, f)
|
|
}
|
|
sort.Strings(formats)
|
|
|
|
for _, format := range formats {
|
|
rendered := renderers[format](entries)
|
|
fr := FormatResult{Format: format}
|
|
if tokenCount != nil {
|
|
fr.Tokens = tokenCount(rendered)
|
|
}
|
|
if ask == nil {
|
|
fr.Skipped = "no LLM provider"
|
|
rep.Formats = append(rep.Formats, fr)
|
|
continue
|
|
}
|
|
for _, q := range questions {
|
|
fr.Asked++
|
|
prompt := buildComprehensionPrompt(format, rendered, q.Question)
|
|
answer, err := ask(prompt)
|
|
if err != nil {
|
|
continue // a provider error counts as an unanswered (incorrect) probe
|
|
}
|
|
if answerAccepts(answer, q.Accept) {
|
|
fr.Correct++
|
|
}
|
|
}
|
|
if fr.Asked > 0 {
|
|
fr.Accuracy = float64(fr.Correct) / float64(fr.Asked)
|
|
}
|
|
rep.Formats = append(rep.Formats, fr)
|
|
}
|
|
return rep
|
|
}
|
|
|
|
func buildComprehensionPrompt(format, rendered, question string) string {
|
|
var b strings.Builder
|
|
fmt.Fprintf(&b, "You are given a code-context pack in %s format.\n", format)
|
|
b.WriteString("Answer the question using ONLY the pack. Reply with just the answer, no preamble.\n\n")
|
|
b.WriteString("=== CONTEXT PACK ===\n")
|
|
b.WriteString(rendered)
|
|
b.WriteString("\n=== END PACK ===\n\n")
|
|
fmt.Fprintf(&b, "Question: %s\nAnswer:", question)
|
|
return b.String()
|
|
}
|
|
|
|
func answerAccepts(answer string, accept []string) bool {
|
|
a := strings.ToLower(answer)
|
|
for _, want := range accept {
|
|
if want == "" {
|
|
continue
|
|
}
|
|
if strings.Contains(a, strings.ToLower(want)) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// ComprehensionMarkdown renders the comprehension report.
|
|
func ComprehensionMarkdown(rep ComprehensionReport) string {
|
|
var b strings.Builder
|
|
fmt.Fprintf(&b, "## Format comprehension (%d questions)\n\n", rep.Questions)
|
|
b.WriteString("| format | accuracy | correct/asked | tokens |\n")
|
|
b.WriteString("|--------|----------|---------------|--------|\n")
|
|
for _, f := range rep.Formats {
|
|
if f.Skipped != "" {
|
|
fmt.Fprintf(&b, "| %s | — | — | %d (skipped: %s) |\n", f.Format, f.Tokens, f.Skipped)
|
|
continue
|
|
}
|
|
fmt.Fprintf(&b, "| %s | %5.1f%% | %d/%d | %d |\n",
|
|
f.Format, f.Accuracy*100, f.Correct, f.Asked, f.Tokens)
|
|
}
|
|
return b.String()
|
|
}
|