Last Bob project of 2025: Docling document processing in Go!
This is the latest project Iβve developed in collaboration with IBM Bob. My goal was to architect a comprehensive document processing ecosystem that leverages the advanced parsing intelligence of Docling. By utilizing Bobβs capabilities, I was able to transition from a conceptual idea to a secure, fully scalable application prototype that bridges the gap between raw unstructured data and machine-ready insights.
Streamlining Document Intelligence: Introducing Docling-Serve Bob
In the evolving landscape of AI-driven data extraction, bridging the gap between sophisticated backend services and seamless user experience is a common challenge. The demonstrator; Docling-Serve Bob, a high-performance desktop application engβ¦
Last Bob project of 2025: Docling document processing in Go!
This is the latest project Iβve developed in collaboration with IBM Bob. My goal was to architect a comprehensive document processing ecosystem that leverages the advanced parsing intelligence of Docling. By utilizing Bobβs capabilities, I was able to transition from a conceptual idea to a secure, fully scalable application prototype that bridges the gap between raw unstructured data and machine-ready insights.
Streamlining Document Intelligence: Introducing Docling-Serve Bob
In the evolving landscape of AI-driven data extraction, bridging the gap between sophisticated backend services and seamless user experience is a common challenge. The demonstrator; Docling-Serve Bob, a high-performance desktop application engineered in Go that brings the power of IBMβs Docling service directly the usersβ fingertips. Built with the sleek Fyne framework, this application transforms the complex task of document conversion into an intuitive, point-and-click experience.
Whether being a developer looking for flexible deployment β ranging from local Python environments to robust containerized clusters β or a power user needing to batch-process documents into Markdown, JSON, or DocTags, Docling-Serve Bob handles the heavy lifting. With built-in server health monitoring, comprehensive error handling, and a focus on speed, it serves as the ultimate bridge between unstructured data and machine-ready insights.
| Feature | Benefit |
| -------------------------- | ------------------------------------------------------------ |
| **Go-Based Performance** | Lightning-fast execution and efficient resource management. |
| **Fyne GUI** | A modern, responsive interface for cross-platform desktop use. |
| **Deployment Flexibility** | Run via local Python, Docker containers, or connect to remote servers. |
| **Rich Output Formats** | Seamlessly export to Markdown, JSON, Text, or DocTags. |
| **Automated Monitoring** | Real-time health checks and detailed logging to ensure uptime. |
Deployment Modes: Flexibility for Every Environment
One of the standout features of Docling-Serve Bob is its ability to adapt to your specific infrastructure. Whether you are running a quick local test or integrating into a production-grade Kubernetes cluster, the application offers three distinct deployment modes:
Containerized (Docker/Podman)
Ideal for users who want a βzero-installβ experience for the Docling backend. In this mode, the application orchestrates a container (using images like quay.io/docling-project/docling-serve) to handle the heavy AI processing.
- Best for: Consistent environments and users who donβt want to manage Python dependencies.
- How it works: Bob manages the container lifecycle, ensuring the docling-serve API is up and healthy before processing starts.
Local Python Environment
For developers who prefer to run services natively, this mode leverages a local Python installation where docling-serve has been installed via pip.
- Best for: Debugging, custom model paths, and environments where containerization isnβt available.
- Setup: Requires a simple
pip install docling-serveon the host machine. Bob then executes the server as a background process.
Remote/Existing Server
If your organization already hosts a centralized Docling instance (on a remote server, OpenShift, or AWS), Bob can simply act as a thin client.
- Best for: Teams sharing a powerful GPU-enabled server or air-gapped enterprise environments.
- Configuration: Simply input the server URL (e.g.,
http://docling.internal.company.com:5001) into the Bob interface to begin converting documents immediately.
Health Monitoring & Auto-Management
Regardless of the mode chosen, Docling-Serve Bob includes an Automatic Server Management layer. It doesnβt just send requests into a void; it performs real-time health checks on the /health endpoint and provides visual feedback through the Fyne-based GUI, ensuring you never start a batch job on a disconnected service.
Batch Processing & Output Formats: Precision at Scale
Converting a single document is easy, but processing hundreds requires a specialized workflow. Docling-Serve Bob is designed to handle high-volume workloads without sacrificing the granular control that researchers and developers need.
The Power of Batch Processing
Instead of manually selecting files one by one, users can feed entire directories into the application. Leveraging Goβs native concurrency primitives, Bob manages the submission of multiple documents to the Docling backend efficiently.
- Queue Management: Monitor the progress of each file in real-time through the GUI.
- Resilience: If one document fails due to corruption, Bob logs the error and continues with the rest of the batch, ensuring your entire pipeline doesnβt stall.
- Efficiency: Automated workflows reduce the manual overhead of document preparation for LLM training or RAG (Retrieval-Augmented Generation) systems.
Versatile Output Formats
Doclingβs core strength lies in its ability to understand the structure of a document β not just the text. Bob exposes these capabilities by allowing users to choose the output format that best fits their downstream application:
- Markdown: Perfect for LLM ingestion and RAG pipelines, preserving headers, lists, and tables in a clean, readable format.
- JSON: The go-to for developers needing programmatic access to the documentβs underlying object model and metadata.
- Text: A simplified, raw extraction for basic search indexing or NLP tasks.
- DocTags: A specialized format that includes semantic tagging of document elements, ideal for training custom models or sophisticated document analysis.
Automatic Server Management & Error Handling
To ensure a βset-and-forgetβ experience, Bob includes a robust management layer that acts as a watchdog for your conversion tasks:
- Health Monitoring: Constant polling of the Docling API ensures the service is ready before a batch starts.
- Comprehensive Logging: Every conversion step is recorded. If a table fails to render or a font is missing, youβll find the exact reason in the logs.
- Visual Feedback: The Fyne-based interface provides immediate visual cues β green for success, red for errors β keeping the user informed without needing to tail a terminal.
Code Implementation
Main Go Application
The main application implemented in Go is represented below ‡οΈ
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"mime/multipart"
"net/http"
"os"
"os/exec"
"path/filepath"
"time"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/widget"
)
const (
defaultDoclingURL = "http://localhost:5001"
outputDir = "./output"
logDir = "./log"
)
var (
logger *log.Logger
doclingServerURL string
serverProcess *exec.Cmd
)
type DoclingClient struct {
baseURL string
client *http.Client
}
func initLogger() error {
if err := os.MkdirAll(logDir, 0755); err != nil {
return fmt.Errorf("failed to create log directory: %w", err)
}
logFileName := filepath.Join(logDir, fmt.Sprintf("docling-serve-bob_%s.log", time.Now().Format("20060102_150405")))
logFile, err := os.OpenFile(logFileName, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
return fmt.Errorf("failed to open log file: %w", err)
}
logger = log.New(logFile, "", log.LstdFlags|log.Lshortfile)
logger.Println("=== Docling-Serve Bob Started ===")
logger.Printf("Log file: %s\n", logFileName)
logger.Printf("Docling server URL: %s\n", doclingServerURL)
logger.Printf("Output directory: %s\n", outputDir)
return nil
}
func NewDoclingClient(baseURL string) *DoclingClient {
logger.Printf("Creating Docling client with base URL: %s\n", baseURL)
client := &DoclingClient{
baseURL: baseURL,
client: &http.Client{Timeout: 300 * time.Second},
}
// Try to discover available endpoints in background
go client.discoverEndpoints()
return client
}
func (dc *DoclingClient) discoverEndpoints() {
logger.Println("Attempting to discover available API endpoints...")
// Try to get OpenAPI/Swagger docs
endpoints := []string{
"/docs",
"/openapi.json",
"/api/docs",
"/swagger.json",
"",
}
for _, endpoint := range endpoints {
url := dc.baseURL + endpoint
resp, err := dc.client.Get(url)
if err != nil {
continue
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
body, _ := io.ReadAll(resp.Body)
logger.Printf("Found endpoint %s (status %d), response length: %d bytes\n", url, resp.StatusCode, len(body))
if len(body) > 0 && len(body) < 10000 {
logger.Printf("Response preview: %s\n", string(body[:min(500, len(body))]))
}
}
}
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
func (dc *DoclingClient) ConvertDocument(filePath string) ([]byte, error) {
logger.Printf("Starting conversion for file: %s\n", filePath)
url := dc.baseURL + "/v1/convert/file"
logger.Printf("Using endpoint: %s\n", url)
file, err := os.Open(filePath)
if err != nil {
logger.Printf("ERROR: Failed to open file %s: %v\n", filePath, err)
return nil, fmt.Errorf("failed to open file: %w", err)
}
defer file.Close()
// Use a pipe to stream the file without loading it all into memory
pr, pw := io.Pipe()
writer := multipart.NewWriter(pw)
// Write the multipart form in a goroutine
go func() {
defer pw.Close()
defer writer.Close()
part, err := writer.CreateFormFile("files", filepath.Base(filePath))
if err != nil {
logger.Printf("ERROR: Failed to create form file: %v\n", err)
pw.CloseWithError(err)
return
}
if _, err := io.Copy(part, file); err != nil {
logger.Printf("ERROR: Failed to copy file content: %v\n", err)
pw.CloseWithError(err)
return
}
}()
req, err := http.NewRequest("POST", url, pr)
if err != nil {
logger.Printf("ERROR: Failed to create request: %v\n", err)
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", writer.FormDataContentType())
req.Header.Set("Transfer-Encoding", "chunked")
logger.Printf("Request headers: %v\n", req.Header)
logger.Println("Sending request with streaming upload...")
resp, err := dc.client.Do(req)
if err != nil {
logger.Printf("ERROR: Failed to send request: %v\n", err)
return nil, fmt.Errorf("failed to send request: %w", err)
}
defer resp.Body.Close()
logger.Printf("Response status: %d\n", resp.StatusCode)
if resp.StatusCode != http.StatusOK {
bodyBytes, _ := io.ReadAll(resp.Body)
logger.Printf("Response body: %s\n", string(bodyBytes))
return nil, fmt.Errorf("server returned status %d: %s", resp.StatusCode, string(bodyBytes))
}
result, err := io.ReadAll(resp.Body)
if err != nil {
logger.Printf("ERROR: Failed to read response: %v\n", err)
return nil, fmt.Errorf("failed to read response: %w", err)
}
logger.Printf("Successfully converted document, response size: %d bytes\n", len(result))
return result, nil
}
func (dc *DoclingClient) ConvertDocumentWithPython(filePath string) ([]byte, error) {
logger.Printf("Starting Python library conversion for file: %s\n", filePath)
// Use the Python converter script
pythonScript := "./docling_converter.py"
// Check if script exists
if _, err := os.Stat(pythonScript); os.IsNotExist(err) {
logger.Printf("ERROR: Python converter script not found: %s\n", pythonScript)
return nil, fmt.Errorf("python converter script not found: %s", pythonScript)
}
// Check if venv exists
venvPython := "./docling-venv/bin/python3"
if _, err := os.Stat(venvPython); os.IsNotExist(err) {
logger.Printf("ERROR: Python venv not found. Please run setup-docling-venv.sh first\n")
return nil, fmt.Errorf("python venv not found. Run setup-docling-venv.sh first")
}
// Execute Python script
cmd := exec.Command(venvPython, pythonScript, filePath, outputDir)
output, err := cmd.CombinedOutput()
if err != nil {
logger.Printf("ERROR: Python conversion failed: %v\nOutput: %s\n", err, string(output))
return nil, fmt.Errorf("python conversion failed: %w\nOutput: %s", err, string(output))
}
logger.Printf("Python conversion completed successfully\n")
logger.Printf("Python output: %s\n", string(output))
return output, nil
}
func ensureOutputDir() error {
if _, err := os.Stat(outputDir); os.IsNotExist(err) {
logger.Printf("Creating output directory: %s\n", outputDir)
if err := os.MkdirAll(outputDir, 0755); err != nil {
logger.Printf("ERROR: Failed to create output directory: %v\n", err)
return fmt.Errorf("failed to create output directory: %w", err)
}
}
return nil
}
func generateOutputFilename(originalPath string) string {
timestamp := time.Now().Format("20060102_150405")
baseName := filepath.Base(originalPath)
ext := filepath.Ext(baseName)
nameWithoutExt := baseName[:len(baseName)-len(ext)]
return fmt.Sprintf("%s_%s.json", nameWithoutExt, timestamp)
}
func saveResult(data []byte, originalPath string) error {
if err := ensureOutputDir(); err != nil {
return err
}
outputFilename := generateOutputFilename(originalPath)
outputPath := filepath.Join(outputDir, outputFilename)
logger.Printf("Saving result to: %s\n", outputPath)
// Pretty print JSON
var prettyJSON bytes.Buffer
if err := json.Indent(&prettyJSON, data, "", " "); err != nil {
// If it's not JSON, save as is
logger.Printf("Data is not JSON, saving as-is\n")
if err := os.WriteFile(outputPath, data, 0644); err != nil {
logger.Printf("ERROR: Failed to write file: %v\n", err)
return err
}
logger.Printf("Successfully saved result to: %s\n", outputPath)
return nil
}
if err := os.WriteFile(outputPath, prettyJSON.Bytes(), 0644); err != nil {
logger.Printf("ERROR: Failed to write file: %v\n", err)
return err
}
logger.Printf("Successfully saved result to: %s\n", outputPath)
return nil
}
func startPodmanServer() error {
logger.Println("Starting Docling server via Podman...")
cmd := exec.Command("podman", "run", "-d", "--rm",
"-p", "5001:5001",
"--name", "docling-serve",
"docker.io/docling/docling-serve:latest")
output, err := cmd.CombinedOutput()
if err != nil {
logger.Printf("ERROR: Failed to start Podman container: %v\nOutput: %s\n", err, string(output))
return fmt.Errorf("failed to start Podman container: %w\nOutput: %s", err, string(output))
}
logger.Printf("Podman container started successfully: %s\n", string(output))
return nil
}
func startPythonVenvServer() error {
logger.Println("Starting Docling server via Python virtual environment...")
// Check if setup script exists
if _, err := os.Stat("./setup-docling-venv.sh"); os.IsNotExist(err) {
return fmt.Errorf("setup-docling-venv.sh not found. Please ensure the script exists")
}
// Check if venv exists, if not run setup
if _, err := os.Stat("./docling-venv"); os.IsNotExist(err) {
logger.Println("Virtual environment not found, running setup...")
setupCmd := exec.Command("bash", "./setup-docling-venv.sh")
setupCmd.Stdout = os.Stdout
setupCmd.Stderr = os.Stderr
if err := setupCmd.Run(); err != nil {
logger.Printf("ERROR: Failed to setup virtual environment: %v\n", err)
return fmt.Errorf("failed to setup virtual environment: %w", err)
}
}
// Start the server
logger.Println("Starting docling-serve from virtual environment...")
serverProcess = exec.Command("bash", "./start-docling-serve.sh")
serverProcess.Stdout = os.Stdout
serverProcess.Stderr = os.Stderr
if err := serverProcess.Start(); err != nil {
logger.Printf("ERROR: Failed to start docling-serve: %v\n", err)
return fmt.Errorf("failed to start docling-serve: %w", err)
}
logger.Printf("Docling-serve started with PID: %d\n", serverProcess.Process.Pid)
// Wait a bit for server to start
time.Sleep(3 * time.Second)
return nil
}
func stopServer() {
if serverProcess != nil && serverProcess.Process != nil {
logger.Println("Stopping docling-serve process...")
serverProcess.Process.Kill()
serverProcess.Wait()
logger.Println("Docling-serve process stopped")
}
}
func showServerSelectionDialog(myApp fyne.App, onComplete func(string)) {
selectionWindow := myApp.NewWindow("Select Docling Server Connection")
selectionWindow.Resize(fyne.NewSize(600, 400))
var selectedOption string
title := widget.NewLabel("Choose how to connect to Docling-Serve:")
title.TextStyle = fyne.TextStyle{Bold: true}
// Option 1: Existing server
option1Label := widget.NewLabel("Connect to existing Docling server")
option1Desc := widget.NewLabel("Use if you already have Docling-Serve running\n(default: http://localhost:5001)")
option1Desc.Wrapping = fyne.TextWrapWord
// Option 2: Podman
option2Label := widget.NewLabel("Start Docling-Serve via Podman")
option2Desc := widget.NewLabel("Automatically starts a Podman container\nRequires: Podman installed and configured")
option2Desc.Wrapping = fyne.TextWrapWord
// Option 3: Python venv
option3Label := widget.NewLabel("Start Docling-Serve via Python Virtual Environment")
option3Desc := widget.NewLabel("Automatically sets up and runs docling-serve in a Python venv\nRequires: Python 3.9+ installed")
option3Desc.Wrapping = fyne.TextWrapWord
// Create clickable containers for each option
option1Container := container.NewVBox(
widget.NewSeparator(),
container.NewHBox(
widget.NewCheck("", func(checked bool) {
if checked {
selectedOption = "existing"
}
}),
container.NewVBox(option1Label, option1Desc),
),
)
option2Container := container.NewVBox(
widget.NewSeparator(),
container.NewHBox(
widget.NewCheck("", func(checked bool) {
if checked {
selectedOption = "podman"
}
}),
container.NewVBox(option2Label, option2Desc),
),
)
option3Container := container.NewVBox(
widget.NewSeparator(),
container.NewHBox(
widget.NewCheck("", func(checked bool) {
if checked {
selectedOption = "venv"
}
}),
container.NewVBox(option3Label, option3Desc),
),
)
continueButton := widget.NewButton("Continue", func() {
if selectedOption == "" {
dialog.ShowInformation("Selection Required", "Please select a connection method", selectionWindow)
return
}
selectionWindow.Close()
onComplete(selectedOption)
})
continueButton.Importance = widget.HighImportance
content := container.NewVBox(
title,
option1Container,
option2Container,
option3Container,
widget.NewSeparator(),
container.NewCenter(continueButton),
)
selectionWindow.SetContent(container.NewPadded(content))
selectionWindow.Show()
}
func main() {
// Initialize logger
if err := initLogger(); err != nil {
fmt.Fprintf(os.Stderr, "Failed to initialize logger: %v\n", err)
os.Exit(1)
}
logger.Println("Application starting...")
myApp := app.New()
// Show server selection dialog first
showServerSelectionDialog(myApp, func(option string) {
logger.Printf("User selected connection method: %s\n", option)
// Set default URL
doclingServerURL = defaultDoclingURL
// Handle server startup based on selection
switch option {
case "podman":
if err := startPodmanServer(); err != nil {
dialog.ShowError(fmt.Errorf("Failed to start Podman server: %w", err), nil)
logger.Printf("ERROR: %v\n", err)
os.Exit(1)
}
// Wait for server to be ready
time.Sleep(5 * time.Second)
case "venv":
if err := startPythonVenvServer(); err != nil {
dialog.ShowError(fmt.Errorf("Failed to start Python venv server: %w", err), nil)
logger.Printf("ERROR: %v\n", err)
os.Exit(1)
}
case "existing":
logger.Println("Using existing Docling server")
}
// Now create the main window
logger.Println("Creating main application window...")
myWindow := myApp.NewWindow("Docling-Serve Client")
myWindow.Resize(fyne.NewSize(900, 600))
logger.Println("Main window created and resized")
// Cleanup on close
myWindow.SetOnClosed(func() {
logger.Println("Main window closed, cleaning up...")
stopServer()
})
logger.Println("Creating Docling client...")
client := NewDoclingClient(doclingServerURL)
logger.Println("Docling client created")
// UI Components
statusLabel := widget.NewLabel("Ready to process documents")
progressBar := widget.NewProgressBar()
progressBar.Hide()
// Add checkbox for Python library mode
usePythonLib := widget.NewCheck("Use Python Library (Advanced: OCR, Tables, Multiple Formats)", func(checked bool) {
if checked {
logger.Println("Python library mode enabled")
statusLabel.SetText("Python library mode: OCR, table extraction, multiple export formats")
} else {
logger.Println("REST API mode enabled")
statusLabel.SetText("Ready to process documents")
}
})
selectedFiles := []string{}
fileList := widget.NewList(
func() int { return len(selectedFiles) },
func() fyne.CanvasObject {
return widget.NewLabel("")
},
func(id widget.ListItemID, obj fyne.CanvasObject) {
obj.(*widget.Label).SetText(filepath.Base(selectedFiles[id]))
},
)
selectButton := widget.NewButton("β Add Document (click multiple times for multiple files)", func() {
logger.Println("User clicked Add Document button")
dialog.ShowFileOpen(func(reader fyne.URIReadCloser, err error) {
if err != nil {
logger.Printf("ERROR: File dialog error: %v\n", err)
dialog.ShowError(err, myWindow)
return
}
if reader == nil {
logger.Println("File dialog cancelled by user")
return
}
defer reader.Close()
filePath := reader.URI().Path()
logger.Printf("User selected file: %s\n", filePath)
// Check if file is already selected
for _, existing := range selectedFiles {
if existing == filePath {
logger.Printf("File already selected: %s\n", filePath)
dialog.ShowInformation("Duplicate", "This file is already selected", myWindow)
return
}
}
selectedFiles = append(selectedFiles, filePath)
fileList.Refresh()
if len(selectedFiles) == 1 {
statusLabel.SetText("1 document selected - click 'Add Document' again to add more")
} else {
statusLabel.SetText(fmt.Sprintf("%d documents selected - click 'Add Document' to add more", len(selectedFiles)))
}
logger.Printf("Total files selected: %d\n", len(selectedFiles))
}, myWindow)
})
clearButton := widget.NewButton("ποΈ Clear All", func() {
logger.Printf("Clearing selection of %d files\n", len(selectedFiles))
selectedFiles = []string{}
fileList.Refresh()
statusLabel.SetText("Ready to process documents")
})
var processButton *widget.Button
processButton = widget.NewButton("βΆοΈ Process All Documents", func() {
if len(selectedFiles) == 0 {
logger.Println("Process button clicked but no files selected")
dialog.ShowInformation("No Files", "Please select at least one document to process", myWindow)
return
}
logger.Printf("Starting batch processing of %d files\n", len(selectedFiles))
processButton.Disable()
selectButton.Disable()
clearButton.Disable()
progressBar.Show()
go func() {
successCount := 0
errorCount := 0
for i, filePath := range selectedFiles {
logger.Printf("Processing file %d/%d: %s\n", i+1, len(selectedFiles), filePath)
statusLabel.SetText(fmt.Sprintf("Processing %d/%d: %s", i+1, len(selectedFiles), filepath.Base(filePath)))
progressBar.SetValue(float64(i) / float64(len(selectedFiles)))
var result []byte
var err error
// Choose conversion method based on checkbox
if usePythonLib.Checked {
result, err = client.ConvertDocumentWithPython(filePath)
} else {
result, err = client.ConvertDocument(filePath)
}
if err != nil {
errorCount++
logger.Printf("ERROR: Failed to convert %s: %v\n", filePath, err)
dialog.ShowError(fmt.Errorf("failed to process %s: %w", filepath.Base(filePath), err), myWindow)
continue
}
// Only save result if using REST API (Python lib saves its own files)
if !usePythonLib.Checked {
if err := saveResult(result, filePath); err != nil {
errorCount++
logger.Printf("ERROR: Failed to save result for %s: %v\n", filePath, err)
dialog.ShowError(fmt.Errorf("failed to save result for %s: %w", filepath.Base(filePath), err), myWindow)
continue
}
}
successCount++
}
progressBar.SetValue(1.0)
statusLabel.SetText(fmt.Sprintf("Completed: %d successful, %d failed", successCount, errorCount))
logger.Printf("Batch processing complete: %d successful, %d failed\n", successCount, errorCount)
processButton.Enable()
selectButton.Enable()
clearButton.Enable()
progressBar.Hide()
if successCount > 0 {
dialog.ShowInformation("Success",
fmt.Sprintf("Processed %d document(s) successfully.\nResults saved to %s\nLogs saved to %s", successCount, outputDir, logDir),
myWindow)
}
}()
})
buttonBox := container.NewHBox(selectButton, clearButton, processButton)
content := container.NewBorder(
container.NewVBox(
widget.NewLabel("Docling-Serve Document Processor"),
widget.NewSeparator(),
usePythonLib,
buttonBox,
statusLabel,
progressBar,
),
nil,
nil,
nil,
fileList,
)
logger.Println("Setting window content...")
myWindow.SetContent(content)
logger.Println("Showing main window...")
myWindow.Show()
logger.Println("Main window should now be visible")
})
myApp.Run()
}
// Made with Bob
Why some part of the application required Python implementation?
While I asked a full βGoβ implementation of the process in Go language, Bob provided a part of the application regarding the batch document processing in Python. The reasons provided are enumerated hereafter;
The Docling document conversion library is a Python package that provides direct access to advanced features:
- OCR (Optical Character Recognition)
- Advanced table structure extraction
- Multiple export formats (JSON, Markdown, Text, DocTags)
- ML model integration for document analysis
from docling.document_converter import DocumentConverter, PdfFormatOption
from docling.datamodel.base_models import InputFormat
from docling.datamodel.pipeline_options import PdfPipelineOptions
No Official Go Bindings
There are no native Go bindings for the Docling library. The only way to access Doclingβs functionality from Go is either:
- Via REST API (docling-serve) β simpler but limited features
- Via Python subprocess execution β full feature access
Advanced Feature Access
The Python script provides features not available through the REST API:
| Feature | REST API | Python Library |
| :------------------------ | :------- | :------------- |
| OCR | β | β
|
| Advanced Table Extraction | β | β
|
| Multiple Export Formats | 1 | 4 |
| Pipeline Configuration | Limited | Full Control |
Architectural Design Pattern
The application uses a hybrid architecture:
βββββββββββββββββββ
β Go GUI App β β User Interface (Fyne)
β (main.go) β β HTTP Client
ββββββββββ¬βββββββββ
β
ββββββ΄βββββ
β β
βΌ βΌ
βββββββββββ ββββββββββββββββββββ
βREST API β β Python Subprocessβ β Advanced Features
β Mode β β (docling_converter.py)
βββββββββββ ββββββββββββββββββββ
From main.go, the Go application executes the Python script as a subprocess:
func (dc *DoclingClient) ConvertDocumentWithPython(filePath string) ([]byte, error) {
pythonScript := "./docling_converter.py"
venvPython := "./docling-venv/bin/python3"
cmd := exec.Command(venvPython, pythonScript, filePath, outputDir)
output, err := cmd.CombinedOutput()
return output, err
}
Separation of Concerns
- Go handles: GUI, user interaction, file management, HTTP communication, process orchestration
- Python handles: Document conversion with ML models, OCR processing, complex data transformations
This separation allows each language to do what it does best. In essence rewriting in Go would require:
β Reimplementing all Docling ML models and algorithms
β Porting complex Python dependencies (PyTorch, transformers, etc.)
β Maintaining feature parity with upstream Docling updates
β Significant development effort with minimal benefit
docling_converter.py exists because:
- β Docling is a Python-native library
- β Provides advanced features unavailable via REST API
- β Enables full pipeline configuration and control
- β Follows a pragmatic hybrid architecture pattern
- β Allows the Go application to leverage Pythonβs ML ecosystem
The design is intentional and optimal β Go provides the excellent GUI and system integration, while Python provides access to the powerful Docling document processing capabilities.
#!/usr/bin/env python3
"""
Docling Document Converter with Advanced Features
This script uses the Docling Python library directly for advanced document conversion
"""
import sys
import json
import time
import logging
from pathlib import Path
from docling.document_converter import DocumentConverter, PdfFormatOption
from docling.datamodel.base_models import InputFormat
from docling.datamodel.pipeline_options import PdfPipelineOptions
# Setup logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
_log = logging.getLogger(__name__)
def convert_document(input_path: str, output_dir: str = "./output") -> dict:
"""
Convert a document using Docling with advanced options
Args:
input_path: Path to input document
output_dir: Directory to save output files
Returns:
dict with conversion results and file paths
"""
try:
# Configure PDF pipeline options
pipeline_options = PdfPipelineOptions()
pipeline_options.do_ocr = True
pipeline_options.do_table_structure = True
# Note: do_cell_matching may not be available in all versions
# pipeline_options.table_structure_options.do_cell_matching = True
# Create document converter with advanced options
doc_converter = DocumentConverter(
allowed_formats=[
InputFormat.PDF,
InputFormat.IMAGE,
InputFormat.DOCX,
InputFormat.HTML,
InputFormat.PPTX,
InputFormat.ASCIIDOC,
InputFormat.MD,
],
format_options={
InputFormat.PDF: PdfFormatOption(pipeline_options=pipeline_options),
},
)
# Convert document
_log.info(f"Converting document: {input_path}")
start_time = time.time()
conv_result = doc_converter.convert(input_path)
end_time = time.time() - start_time
_log.info(f"Document converted in {end_time:.2f} seconds.")
# Prepare output directory
output_path = Path(output_dir)
output_path.mkdir(parents=True, exist_ok=True)
doc_filename = conv_result.input.file.stem
# Export to multiple formats
output_files = {}
# Export Deep Search document JSON format
json_file = output_path / f"{doc_filename}.json"
with json_file.open("w", encoding="utf-8") as fp:
fp.write(json.dumps(conv_result.document.export_to_dict(), indent=2))
output_files['json'] = str(json_file)
_log.info(f"Exported JSON: {json_file}")
# Export Text format
text_file = output_path / f"{doc_filename}.txt"
with text_file.open("w", encoding="utf-8") as fp:
fp.write(conv_result.document.export_to_text())
output_files['text'] = str(text_file)
_log.info(f"Exported Text: {text_file}")
# Export Markdown format
md_file = output_path / f"{doc_filename}.md"
with md_file.open("w", encoding="utf-8") as fp:
fp.write(conv_result.document.export_to_markdown())
output_files['markdown'] = str(md_file)
_log.info(f"Exported Markdown: {md_file}")
# Export Document Tags format
doctags_file = output_path / f"{doc_filename}.doctags"
with doctags_file.open("w", encoding="utf-8") as fp:
fp.write(conv_result.document.export_to_document_tokens())
output_files['doctags'] = str(doctags_file)
_log.info(f"Exported DocTags: {doctags_file}")
# Return results
return {
"status": "success",
"input_file": input_path,
"processing_time": end_time,
"output_files": output_files,
"document_info": {
"filename": doc_filename,
"page_count": len(conv_result.document.pages) if hasattr(conv_result.document, 'pages') else None,
}
}
except Exception as e:
_log.error(f"Error converting document: {e}", exc_info=True)
return {
"status": "error",
"input_file": input_path,
"error": str(e)
}
def main():
"""Main entry point for CLI usage"""
if len(sys.argv) < 2:
print("Usage: python docling_converter.py <input_file> [output_dir]", file=sys.stderr)
sys.exit(1)
input_file = sys.argv[1]
output_dir = sys.argv[2] if len(sys.argv) > 2 else "./output"
# Convert document
result = convert_document(input_file, output_dir)
# Print result as JSON
print(json.dumps(result, indent=2))
# Exit with appropriate code
sys.exit(0 if result["status"] == "success" else 1)
if __name__ == "__main__":
main()
# Made with Bob
Conclusion: Redefining Document Workflows
By combining the structural intelligence of IBMβs Docling with the performance and portability of Go, Docling-Serve Bob eliminates the friction often associated with document AI. Itβs no longer just about extracting text; itβs about preserving the βsoulβ of a document β its tables, its hierarchy, and its context β all through a user-friendly GUI that fits right into a developerβs daily toolkit.
Whether youβre building a RAG pipeline or simply need to clean up a mountain of legacy PDFs, this application provides the flexibility to deploy anywhere and the power to process at scale.
Next Steps: Get Started with Docling-Serve Bob
Ready to transform your document processing? Here is how to hit the ground running:
- Clone the Repository: Visit the GitHub repo and pull the latest Go source code.
- Install Fyne: Ensure you have the Fyne.io prerequisites installed for your OS (Windows, macOS, or Linux).
- Choose Your Backend: * If you have Docker, Bob will handle the rest (if you prefer Python, run pip install docling-serve).
- Run the App: Execute go run . and start your first batch conversion!
Thanks for reading π€
Links
- GitHub Projet Repository: https://github.com/aairom/doclingserve-bob
- Docling Project: https://github.com/docling-project/docling
- IBM Project Bob: https://www.ibm.com/products/bob