Skip to content

Plugins

foliq is a small engine with sockets. Every socket can be filled by a built-in plugin, your own Python class, or your own Rust code.

Socket Does Built in
Source lists files (with versions) and reads them folders
Parser turns bytes into Markdown Markdown, HTML, DOCX, PPTX, XLSX, PDF
Splitter cuts a document into chunks one chunk per section
Store saves and searches documents and chunks SQLite with full-text search
Sink receives every change after a sync text mirror, JSON Lines

Python plugins work through foliq.Index. The foliq command runs Rust code only, so it uses the built-in plugins and Rust plugins compiled into it.

Python

A plugin is any object with the right method. There is nothing to inherit.

import csv
import io

import foliq


class CsvParser:
    name = "csv"
    version = "1"           # change it to re-parse every CSV file
    extensions = ("csv",)   # tried before the built-in parsers

    def parse(self, data: bytes, path: str) -> str:
        rows = list(csv.reader(io.StringIO(data.decode())))
        lines = ["| " + " | ".join(row) + " |" for row in rows]
        lines.insert(1, "|" + " --- |" * len(rows[0]))
        return "\n".join(lines)  # or {"text": ..., "title": ..., "metadata": {...}}


class ParagraphSplitter:
    def split(self, text: str) -> list[str]:
        # Return pieces of the text, in order. foliq adds the lines, pages and headings.
        return [paragraph for paragraph in text.split("\n\n") if paragraph.strip()]


class PrintSink:
    def apply(self, changes: list[dict]) -> None:
        for change in changes:
            # {"type": "upsert", "document": {...}, "chunks": [...]}
            # {"type": "delete", "document_id": ..., "collection": ..., "path": ...}
            print(change["type"], change.get("document", change).get("path"))


index = foliq.Index(parsers=[CsvParser()], splitter=ParagraphSplitter(), sinks=[PrintSink()])

Parsers run in parallel threads; foliq takes the GIL for each call. An exception in a plugin fails only that file, and the sync report says why.

Rust

Rust plugins implement a trait from the foliq-core crate and are added with Index::builder:

//! A Rust plugin: a parser for `.csv` files, used by an index.
//!
//! Run it with `cargo run -p foliq --example custom_parser -- <folder>`.

use std::sync::Arc;

use foliq::{Index, Parsed, Parser, Result, SearchOptions, SourceEntry};

/// Turns CSV files into Markdown tables.
struct CsvParser;

impl Parser for CsvParser {
    fn name(&self) -> &str {
        "csv"
    }

    fn extensions(&self) -> Vec<String> {
        vec!["csv".to_string()]
    }

    fn parse(&self, bytes: &[u8], _entry: &SourceEntry) -> Result<Parsed> {
        let text = String::from_utf8_lossy(bytes);
        let mut markdown = String::new();
        for (number, line) in text.lines().enumerate() {
            let cells: Vec<&str> = line.split(',').collect();
            markdown.push_str(&format!("| {} |\n", cells.join(" | ")));
            if number == 0 {
                markdown.push_str(&format!("|{}\n", " --- |".repeat(cells.len())));
            }
        }
        // Return Markdown: foliq finds the headings, tables and pages in it.
        Ok(Parsed::markdown(markdown))
    }
}

fn main() -> Result<()> {
    let folder = std::env::args().nth(1).unwrap_or_else(|| ".".to_string());
    let index = Index::builder(".foliq").parser(Arc::new(CsvParser)).open()?;
    index.add_folder(folder.as_ref(), None, Vec::new())?;
    let report = index.sync()?;
    println!("{} added, {} updated", report.added, report.updated);
    for result in index.search("price", &SearchOptions::default())? {
        println!("{}", result.citation);
    }
    Ok(())
}

The traits are small: see crates/foliq-core/src. Coming next: Rust plugins packaged as Python wheels, so pip install foliq-something adds a fast plugin to foliq.Index.