Files
Tracker/Code/Tracker/pipeline/pipeline.go
T
MangoPig 991d5a0eae Nil fix
2026-02-22 00:20:00 +00:00

336 lines
8.9 KiB
Go

package pipeline
import (
"fmt"
"log"
"os"
"path/filepath"
"strings"
"time"
"github.com/goko/jingtian-tracker/clients"
"github.com/goko/jingtian-tracker/config"
"github.com/goko/jingtian-tracker/db"
"github.com/goko/jingtian-tracker/watcher"
)
// TrackerColumns defines the tracker spreadsheet columns
var TrackerColumns = []string{
"Document Path",
"Document Type",
"Client Name",
"Matter Reference",
"TM Number",
"Trademark Name",
"Trademark Class",
"Filing Date",
"Response Deadline",
"Hearing Date",
"Amount",
"Currency",
"Status",
"First Processed",
"Last Updated",
"Update Source",
"Processing Notes",
}
type Pipeline struct {
cfg *config.Config
db *db.DB
watcher *watcher.Watcher
docling *clients.DoclingClient
ollama *clients.OllamaClient
tools *clients.ToolsClient
errorLog *os.File
}
func New(cfg *config.Config, database *db.DB, w *watcher.Watcher) (*Pipeline, error) {
// Open error log file
var errorLog *os.File
if cfg.Logging.ErrorFile != "" {
// Ensure directory exists
dir := filepath.Dir(cfg.Logging.ErrorFile)
if err := os.MkdirAll(dir, 0755); err != nil {
return nil, fmt.Errorf("failed to create log directory: %w", err)
}
f, err := os.OpenFile(cfg.Logging.ErrorFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return nil, fmt.Errorf("failed to open error log: %w", err)
}
errorLog = f
}
return &Pipeline{
cfg: cfg,
db: database,
watcher: w,
docling: clients.NewDoclingClient(cfg.Services.Docling),
ollama: clients.NewOllamaClient(cfg.Services.Ollama, cfg.Ollama.Model, cfg.Ollama.Temperature),
tools: clients.NewToolsClient(cfg.Services.Tools),
errorLog: errorLog,
}, nil
}
func (p *Pipeline) Close() {
if p.errorLog != nil {
p.errorLog.Close()
}
}
func (p *Pipeline) Run() error {
// Check service health
if err := p.checkServices(); err != nil {
return fmt.Errorf("service health check failed: %w", err)
}
// Ensure tracker exists
if err := p.ensureTracker(); err != nil {
log.Printf("[pipeline] Warning: could not ensure tracker: %v", err)
// Don't fail - tracker might be created manually
}
// Start watcher
if err := p.watcher.Start(); err != nil {
return fmt.Errorf("failed to start watcher: %w", err)
}
log.Println("[pipeline] Started processing loop")
// Process events
for event := range p.watcher.Events {
p.processFile(event)
}
return nil
}
func (p *Pipeline) checkServices() error {
log.Println("[pipeline] Checking service health...")
if err := p.docling.Health(); err != nil {
return fmt.Errorf("docling: %w", err)
}
log.Println("[pipeline] Docling: OK")
if err := p.ollama.Health(); err != nil {
return fmt.Errorf("ollama: %w", err)
}
log.Println("[pipeline] Ollama: OK")
if err := p.tools.Health(); err != nil {
return fmt.Errorf("tools: %w", err)
}
log.Println("[pipeline] Tools: OK")
return nil
}
func (p *Pipeline) ensureTracker() error {
// Check if tracker exists by trying to read it
_, err := p.tools.ReadExcel(p.cfg.Tracker.Path, p.cfg.Tracker.Sheet, p.cfg.Tracker.HeaderRow)
if err != nil {
// Try to create it
log.Printf("[pipeline] Creating tracker: %s", p.cfg.Tracker.Path)
if createErr := p.tools.CreateTracker(p.cfg.Tracker.Path, p.cfg.Tracker.Sheet, TrackerColumns); createErr != nil {
return fmt.Errorf("failed to create tracker: %w", createErr)
}
log.Printf("[pipeline] Tracker created successfully")
}
return nil
}
func (p *Pipeline) processFile(event watcher.FileEvent) {
// Get relative path
relPath, err := filepath.Rel(p.cfg.Watch.Root, event.Path)
if err != nil {
relPath = event.Path
}
log.Printf("[pipeline] Processing: %s", relPath)
// Check if processing needed
needsProcessing, err := p.db.NeedsProcessing(relPath, event.Hash)
if err != nil {
p.logError(relPath, fmt.Sprintf("failed to check processing status: %v", err))
return
}
if !needsProcessing {
log.Printf("[pipeline] Skipping (already processed with same hash): %s", relPath)
return
}
// Upsert document record
doc, err := p.db.UpsertDocument(relPath, event.Hash, event.Size, event.ModTime)
if err != nil {
p.logError(relPath, fmt.Sprintf("failed to upsert document: %v", err))
return
}
// Update status to processing
p.db.UpdateStatus(doc.ID, "processing")
// Process based on file type
ext := strings.ToLower(filepath.Ext(event.Path))
var extracted map[string]interface{}
switch ext {
case ".xlsx", ".xls":
// For Excel files, we don't OCR, just log that we saw it
log.Printf("[pipeline] Excel file detected, skipping OCR: %s", relPath)
p.db.LogAction(doc.ID, "skipped", "Excel files are not processed through OCR")
p.db.MarkProcessed(doc.ID)
return
default:
// Send to Docling for OCR/extraction
content, doclingErr := p.docling.Convert(event.Path)
if doclingErr != nil {
p.logError(relPath, fmt.Sprintf("Docling failed: %v", doclingErr))
p.db.UpdateStatus(doc.ID, "failed")
p.db.LogAction(doc.ID, "failed", fmt.Sprintf("Docling error: %v", doclingErr))
return
}
if content == "" {
p.logError(relPath, "Docling returned empty content")
p.db.UpdateStatus(doc.ID, "failed")
p.db.LogAction(doc.ID, "failed", "Empty content from Docling")
return
}
// Detect document type from extension
docType := detectDocType(ext)
// Send to Ollama for extraction
extracted, err = p.ollama.Extract(content, docType)
if err != nil {
p.logError(relPath, fmt.Sprintf("Ollama failed: %v", err))
p.db.UpdateStatus(doc.ID, "failed")
p.db.LogAction(doc.ID, "failed", fmt.Sprintf("Ollama error: %v", err))
return
}
}
// Save extraction to database
if err := p.db.SaveExtraction(doc.ID, extracted); err != nil {
p.logError(relPath, fmt.Sprintf("failed to save extraction: %v", err))
}
// Write to tracker
if err := p.writeToTracker(relPath, extracted); err != nil {
p.logError(relPath, fmt.Sprintf("failed to write to tracker: %v", err))
p.db.UpdateStatus(doc.ID, "failed")
p.db.LogAction(doc.ID, "failed", fmt.Sprintf("Tracker write error: %v", err))
return
}
// Mark as completed
p.db.MarkProcessed(doc.ID)
p.db.LogAction(doc.ID, "processed", "Successfully processed and added to tracker")
log.Printf("[pipeline] Successfully processed: %s", relPath)
}
func (p *Pipeline) writeToTracker(docPath string, extracted map[string]interface{}) error {
// Check if row exists for this document
existing, err := p.tools.FindRow(
p.cfg.Tracker.Path,
p.cfg.Tracker.Sheet,
p.cfg.Tracker.HeaderRow,
"Document Path",
docPath,
)
if err != nil {
// Row not found is OK - we'll add a new one
existing = &clients.ExcelFindResponse{Found: false}
}
now := time.Now().Format("02-01-2006 15:04:05")
// Build row data
row := map[string]interface{}{
"Document Path": docPath,
"Document Type": getStringField(extracted, "document_type"),
"Client Name": getStringField(extracted, "client_name"),
"Matter Reference": getStringField(extracted, "matter_reference"),
"TM Number": getStringField(extracted, "tm_number"),
"Trademark Name": getStringField(extracted, "trademark_name"),
"Trademark Class": getStringField(extracted, "trademark_class"),
"Filing Date": getStringField(extracted, "filing_date"),
"Response Deadline": getStringField(extracted, "response_deadline"),
"Hearing Date": getStringField(extracted, "hearing_date"),
"Amount": getStringField(extracted, "amount"),
"Currency": getStringField(extracted, "currency"),
"Status": getStringField(extracted, "status"),
"Last Updated": now,
"Update Source": "Pipeline",
"Processing Notes": "",
}
if existing.Found {
// Check if manually edited
if updateSource, ok := existing.Row["Update Source"].(string); ok && updateSource == "Manual" {
log.Printf("[pipeline] Skipping update (manually edited): %s", docPath)
return nil
}
// Update existing row
// Preserve First Processed
if fp, ok := existing.Row["First Processed"]; ok {
row["First Processed"] = fp
}
return p.tools.UpdateRow(
p.cfg.Tracker.Path,
p.cfg.Tracker.Sheet,
p.cfg.Tracker.HeaderRow,
existing.RowNum,
row,
)
}
// Add new row
row["First Processed"] = now
return p.tools.AddRow(
p.cfg.Tracker.Path,
p.cfg.Tracker.Sheet,
p.cfg.Tracker.HeaderRow,
row,
)
}
func (p *Pipeline) logError(path, msg string) {
errMsg := fmt.Sprintf("[%s] %s: %s", time.Now().Format("2006-01-02 15:04:05"), path, msg)
log.Printf("[pipeline] ERROR: %s", errMsg)
if p.errorLog != nil {
p.errorLog.WriteString(errMsg + "\n")
}
}
func detectDocType(ext string) string {
switch ext {
case ".pdf":
return "PDF document"
case ".docx", ".doc":
return "Word document"
case ".png", ".jpg", ".jpeg":
return "Image/screenshot"
default:
return "Document"
}
}
func getStringField(m map[string]interface{}, key string) string {
v, ok := m[key]
if !ok || v == nil {
return ""
}
if s, ok := v.(string); ok {
return s
}
return fmt.Sprintf("%v", v)
}