Compare commits
No commits in common. "v0.1.8" and "main" have entirely different histories.
|
@ -1 +0,0 @@
|
||||||
*.swp
|
|
19
README.md
19
README.md
|
@ -9,8 +9,6 @@ Go package for automated evaluation of academic papers using LLM-based criteria
|
||||||
- Rate limiting with request delay configuration
|
- Rate limiting with request delay configuration
|
||||||
- File-based processing (JSON input/output)
|
- File-based processing (JSON input/output)
|
||||||
- Customizable evaluation criteria
|
- Customizable evaluation criteria
|
||||||
- Robust error handling with failure tracking
|
|
||||||
- Automatic dump file for failed analyses
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
@ -21,7 +19,7 @@ go get gitea.r8z.us/stwhite/paperprocessor
|
||||||
## API Reference
|
## API Reference
|
||||||
|
|
||||||
### ProcessFile
|
### 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
|
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
|
- outputPath: Path to write processing results JSON
|
||||||
- criteriaPath: Path to text file with evaluation criteria
|
- criteriaPath: Path to text file with evaluation criteria
|
||||||
- config: Configuration settings for API and processing
|
- config: Configuration settings for API and processing
|
||||||
|
- debug: Enable debug logging when true
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
- error: Processing error or nil if successful
|
- error: Processing error or nil if successful
|
||||||
|
@ -55,6 +54,7 @@ err := paperprocessor.ProcessFile(
|
||||||
"output/results.json",
|
"output/results.json",
|
||||||
"criteria.txt",
|
"criteria.txt",
|
||||||
config,
|
config,
|
||||||
|
true, // debug mode
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("Processing failed:", err)
|
log.Fatal("Processing failed:", err)
|
||||||
|
@ -106,23 +106,10 @@ Evaluation criteria:
|
||||||
"decision": "REJECT",
|
"decision": "REJECT",
|
||||||
"explanation": "Doesn't meet novelty requirements..."
|
"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
|
## Configuration Options
|
||||||
|
|
||||||
| Parameter | Description | Default |
|
| Parameter | Description | Default |
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
"unicode"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Paper represents a single academic paper
|
// Paper represents a single academic paper
|
||||||
|
@ -28,11 +29,6 @@ type PaperResult struct {
|
||||||
type ProcessingResult struct {
|
type ProcessingResult struct {
|
||||||
Accepted []PaperResult `json:"accepted"`
|
Accepted []PaperResult `json:"accepted"`
|
||||||
Rejected []PaperResult `json:"rejected"`
|
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
|
// 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)
|
decision, err := p.evaluatePaper(paper, criteria)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Instead of returning error, add to failed list
|
return nil, fmt.Errorf("error processing paper %s: %v", paper.ArxivID, err)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
paperResult := PaperResult{
|
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
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,14 +120,12 @@ type llmResponse struct {
|
||||||
type decisionResult struct {
|
type decisionResult struct {
|
||||||
Decision string
|
Decision string
|
||||||
Explanation string
|
Explanation string
|
||||||
RawOutput string // Store raw output for error reporting
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Processor) evaluatePaper(paper Paper, criteria string) (*decisionResult, error) {
|
func (p *Processor) evaluatePaper(paper Paper, criteria string) (*decisionResult, error) {
|
||||||
prompt := fmt.Sprintf(`Please evaluate the following academic paper against the provided criteria.
|
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.
|
Respond with either "ACCEPT" or "REJECT" followed by a brief 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 emphasis in your response. Keep your response clear and concise.
|
||||||
Do not use markdown, bullet points, or quotes in your response. Keep your response clear and concise.
|
|
||||||
Your response should be in the format:
|
Your response should be in the format:
|
||||||
DECISION
|
DECISION
|
||||||
Explanation
|
Explanation
|
||||||
|
@ -172,12 +148,12 @@ Abstract: %s`, criteria, paper.Title, paper.Abstract)
|
||||||
|
|
||||||
reqJSON, err := json.Marshal(reqBody)
|
reqJSON, err := json.Marshal(reqBody)
|
||||||
if err != nil {
|
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))
|
req, err := http.NewRequest("POST", p.config.APIEndpoint, bytes.NewBuffer(reqJSON))
|
||||||
if err != nil {
|
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")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
@ -186,63 +162,51 @@ Abstract: %s`, criteria, paper.Title, paper.Abstract)
|
||||||
client := &http.Client{}
|
client := &http.Client{}
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
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()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
body, err := ioutil.ReadAll(resp.Body)
|
body, err := ioutil.ReadAll(resp.Body)
|
||||||
if err != nil {
|
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
|
var llmResp llmResponse
|
||||||
if err := json.Unmarshal(body, &llmResp); err != nil {
|
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 {
|
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
|
content := llmResp.Choices[0].Message.Content
|
||||||
|
|
||||||
// Find line with ACCEPT/REJECT
|
// Find first line with ACCEPT/REJECT
|
||||||
var decisionLine string
|
var decisionLine string
|
||||||
lines := bytes.Split([]byte(content), []byte("\n"))
|
lines := bytes.Split([]byte(content), []byte("\n"))
|
||||||
for i, line := range lines {
|
for _, line := range lines {
|
||||||
upperLine := strings.ToUpper(string(line))
|
if strings.Contains(strings.ToUpper(string(line)), "ACCEPT") ||
|
||||||
// Check current line
|
strings.Contains(strings.ToUpper(string(line)), "REJECT") {
|
||||||
if strings.Contains(upperLine, "ACCEPT") || strings.Contains(upperLine, "REJECT") {
|
|
||||||
decisionLine = string(line)
|
decisionLine = string(line)
|
||||||
break
|
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 == "" {
|
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
|
// Clean and normalize decision
|
||||||
rawDecision := strings.TrimSpace(decisionLine)
|
rawDecision := strings.TrimSpace(decisionLine)
|
||||||
|
|
||||||
// Handle common prefixes and clean the decision text
|
// Remove "DECISION:" prefix if present and trim non-alphabetic characters
|
||||||
cleanDecision := rawDecision
|
cleanDecision := strings.TrimPrefix(rawDecision, "DECISION:")
|
||||||
for _, prefix := range []string{"DECISION:", "Decision:", "-", "\"", "*", "THIS PAPER IS"} {
|
cleanDecision = strings.TrimFunc(cleanDecision, func(r rune) bool {
|
||||||
cleanDecision = strings.TrimPrefix(cleanDecision, prefix)
|
return !unicode.IsLetter(r) && !unicode.IsNumber(r)
|
||||||
}
|
})
|
||||||
cleanDecision = strings.TrimSpace(cleanDecision)
|
|
||||||
// Remove any remaining quotes
|
|
||||||
cleanDecision = strings.Trim(cleanDecision, "\"")
|
|
||||||
|
|
||||||
// Normalize case
|
// Normalize case and check for valid decision
|
||||||
upperDecision := strings.ToUpper(cleanDecision)
|
upperDecision := strings.ToUpper(cleanDecision)
|
||||||
var decision string
|
var decision string
|
||||||
switch {
|
switch {
|
||||||
|
@ -251,25 +215,16 @@ Abstract: %s`, criteria, paper.Title, paper.Abstract)
|
||||||
case strings.HasPrefix(upperDecision, "REJECT"):
|
case strings.HasPrefix(upperDecision, "REJECT"):
|
||||||
decision = "REJECT"
|
decision = "REJECT"
|
||||||
default:
|
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)
|
rawDecision, cleanDecision, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get explanation as everything after the decision line
|
// Get explanation as everything after the decision line
|
||||||
explanation := strings.TrimSpace(strings.Replace(content, decisionLine, "", 1))
|
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{
|
return &decisionResult{
|
||||||
Decision: decision,
|
Decision: decision,
|
||||||
Explanation: explanation,
|
Explanation: explanation,
|
||||||
RawOutput: content,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue