Compare commits

..

No commits in common. "v0.1.8" and "main" have entirely different histories.
v0.1.8 ... main

3 changed files with 25 additions and 84 deletions

1
.gitignore vendored
View File

@ -1 +0,0 @@
*.swp

View File

@ -9,8 +9,6 @@ Go package for automated evaluation of academic papers using LLM-based criteria
- Rate limiting with request delay configuration
- File-based processing (JSON input/output)
- Customizable evaluation criteria
- Robust error handling with failure tracking
- Automatic dump file for failed analyses
## Installation
@ -21,7 +19,7 @@ go get gitea.r8z.us/stwhite/paperprocessor
## API Reference
### ProcessFile
`func ProcessFile(inputPath, outputPath, criteriaPath string, config Config) error`
`func ProcessFile(inputPath, outputPath, criteriaPath string, config Config, debug bool) error`
Processes papers from input JSON file and writes results to output JSON file
@ -30,6 +28,7 @@ Parameters:
- outputPath: Path to write processing results JSON
- criteriaPath: Path to text file with evaluation criteria
- config: Configuration settings for API and processing
- debug: Enable debug logging when true
Returns:
- error: Processing error or nil if successful
@ -55,6 +54,7 @@ err := paperprocessor.ProcessFile(
"output/results.json",
"criteria.txt",
config,
true, // debug mode
)
if err != nil {
log.Fatal("Processing failed:", err)
@ -106,23 +106,10 @@ Evaluation criteria:
"decision": "REJECT",
"explanation": "Doesn't meet novelty requirements..."
}
],
"failed": [
{
"paper": {
"title": "Problematic Paper",
"abstract": "...",
"arxiv_id": "2301.11111"
},
"error": "invalid decision format",
"output": ""
}
]
}
```
When papers fail processing, they are added to the "failed" list in the output JSON and also written to a `dump.json` file for detailed review.
## Configuration Options
| Parameter | Description | Default |

View File

@ -8,6 +8,7 @@ import (
"net/http"
"strings"
"time"
"unicode"
)
// Paper represents a single academic paper
@ -28,11 +29,6 @@ type PaperResult struct {
type ProcessingResult struct {
Accepted []PaperResult `json:"accepted"`
Rejected []PaperResult `json:"rejected"`
Failed []struct {
Paper Paper `json:"paper"`
Error string `json:"error"`
Output string `json:"output"`
} `json:"failed"`
}
// Config holds the configuration for the processor
@ -84,17 +80,7 @@ func (p *Processor) ProcessPapers(papers []Paper, criteria string) (*ProcessingR
}
decision, err := p.evaluatePaper(paper, criteria)
if err != nil {
// Instead of returning error, add to failed list
result.Failed = append(result.Failed, struct {
Paper Paper `json:"paper"`
Error string `json:"error"`
Output string `json:"output"`
}{
Paper: paper,
Error: err.Error(),
Output: decision.RawOutput, // Include raw output for debugging
})
continue
return nil, fmt.Errorf("error processing paper %s: %v", paper.ArxivID, err)
}
paperResult := PaperResult{
@ -110,14 +96,6 @@ func (p *Processor) ProcessPapers(papers []Paper, criteria string) (*ProcessingR
}
}
// Write failed analyses to dump file if any exist
if len(result.Failed) > 0 {
dumpData, err := json.MarshalIndent(result.Failed, "", " ")
if err == nil { // Only try to write if marshaling succeeded
ioutil.WriteFile("dump.json", dumpData, 0644)
}
}
return result, nil
}
@ -142,14 +120,12 @@ type llmResponse struct {
type decisionResult struct {
Decision string
Explanation string
RawOutput string // Store raw output for error reporting
}
func (p *Processor) evaluatePaper(paper Paper, criteria string) (*decisionResult, error) {
prompt := fmt.Sprintf(`Please evaluate the following academic paper against the provided criteria.
Respond with either "ACCEPT" or "REJECT" followed by an explanation of your decision.
For ACCEPT decisions, provide a thorough explanation. For REJECT decisions, keep the explanation brief and focused on the key reason.
Do not use markdown, bullet points, or quotes in your response. Keep your response clear and concise.
Respond with either "ACCEPT" or "REJECT" followed by a brief explanation of your decision.
Do not use markdown emphasis in your response. Keep your response clear and concise.
Your response should be in the format:
DECISION
Explanation
@ -172,12 +148,12 @@ Abstract: %s`, criteria, paper.Title, paper.Abstract)
reqJSON, err := json.Marshal(reqBody)
if err != nil {
return &decisionResult{RawOutput: ""}, fmt.Errorf("error marshaling request: %v", err)
return nil, fmt.Errorf("error marshaling request: %v", err)
}
req, err := http.NewRequest("POST", p.config.APIEndpoint, bytes.NewBuffer(reqJSON))
if err != nil {
return &decisionResult{RawOutput: ""}, fmt.Errorf("error creating request: %v", err)
return nil, fmt.Errorf("error creating request: %v", err)
}
req.Header.Set("Content-Type", "application/json")
@ -186,63 +162,51 @@ Abstract: %s`, criteria, paper.Title, paper.Abstract)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return &decisionResult{RawOutput: ""}, fmt.Errorf("error making request: %v", err)
return nil, fmt.Errorf("error making request: %v", err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return &decisionResult{RawOutput: ""}, fmt.Errorf("error reading response: %v", err)
return nil, fmt.Errorf("error reading response: %v", err)
}
var llmResp llmResponse
if err := json.Unmarshal(body, &llmResp); err != nil {
return &decisionResult{RawOutput: string(body)}, fmt.Errorf("error unmarshaling response: %v", err)
return nil, fmt.Errorf("error unmarshaling response: %v", err)
}
if len(llmResp.Choices) == 0 {
return &decisionResult{RawOutput: string(body)}, fmt.Errorf("no response from LLM")
return nil, fmt.Errorf("no response from LLM")
}
content := llmResp.Choices[0].Message.Content
// Find line with ACCEPT/REJECT
// Find first line with ACCEPT/REJECT
var decisionLine string
lines := bytes.Split([]byte(content), []byte("\n"))
for i, line := range lines {
upperLine := strings.ToUpper(string(line))
// Check current line
if strings.Contains(upperLine, "ACCEPT") || strings.Contains(upperLine, "REJECT") {
for _, line := range lines {
if strings.Contains(strings.ToUpper(string(line)), "ACCEPT") ||
strings.Contains(strings.ToUpper(string(line)), "REJECT") {
decisionLine = string(line)
break
}
// If current line is "DECISION", check next line
if strings.TrimSpace(upperLine) == "DECISION" && i+1 < len(lines) {
nextLine := strings.ToUpper(string(lines[i+1]))
if strings.Contains(nextLine, "ACCEPT") || strings.Contains(nextLine, "REJECT") {
decisionLine = string(lines[i+1])
break
}
}
}
if decisionLine == "" {
return &decisionResult{RawOutput: content}, fmt.Errorf("no decision found in response. Full response:\n%s", content)
return nil, fmt.Errorf("no decision found in response. Full response:\n%s", content)
}
// Clean and normalize decision
rawDecision := strings.TrimSpace(decisionLine)
// Handle common prefixes and clean the decision text
cleanDecision := rawDecision
for _, prefix := range []string{"DECISION:", "Decision:", "-", "\"", "*", "THIS PAPER IS"} {
cleanDecision = strings.TrimPrefix(cleanDecision, prefix)
}
cleanDecision = strings.TrimSpace(cleanDecision)
// Remove any remaining quotes
cleanDecision = strings.Trim(cleanDecision, "\"")
// Remove "DECISION:" prefix if present and trim non-alphabetic characters
cleanDecision := strings.TrimPrefix(rawDecision, "DECISION:")
cleanDecision = strings.TrimFunc(cleanDecision, func(r rune) bool {
return !unicode.IsLetter(r) && !unicode.IsNumber(r)
})
// Normalize case
// Normalize case and check for valid decision
upperDecision := strings.ToUpper(cleanDecision)
var decision string
switch {
@ -251,25 +215,16 @@ Abstract: %s`, criteria, paper.Title, paper.Abstract)
case strings.HasPrefix(upperDecision, "REJECT"):
decision = "REJECT"
default:
return &decisionResult{RawOutput: content}, fmt.Errorf("invalid decision value: %q (cleaned: %q). Full response:\n%s",
return nil, fmt.Errorf("invalid decision value: %q (cleaned: %q). Full response:\n%s",
rawDecision, cleanDecision, content)
}
// Get explanation as everything after the decision line
explanation := strings.TrimSpace(strings.Replace(content, decisionLine, "", 1))
// Remove any "Explanation" header if present
explanation = strings.TrimPrefix(strings.TrimSpace(explanation), "Explanation")
explanation = strings.TrimSpace(explanation)
if explanation == "" {
return &decisionResult{RawOutput: content}, fmt.Errorf("empty explanation in response. Full response:\n%s", content)
}
return &decisionResult{
Decision: decision,
Explanation: explanation,
RawOutput: content,
}, nil
}