Skip to content
joel710Public

About

Numera is a self-hosted accounting intelligence system powered by an LLM agentic loop. It connects any OpenAI-compatible model to structured financial data, live web search, and user-uploaded documents (CSV, XLSX, DOCX, PDF, images). All responses are in French with FCFA amounts purpose-built for businesses under UEMOA, CEMAC, and SYSCOHADA juri

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

7 Commits

Folders and files

Repository files navigation

Numera

A self-hosted accounting intelligence system built on a tool-calling agentic loop. Numera connects any OpenAI-compatible language model to structured financial data, live web search, and user-uploaded documents. All responses are in French with amounts converted to FCFA, making it directly applicable to businesses operating under UEMOA, CEMAC, and SYSCOHADA jurisdictions.

Zero required runtime dependencies. No framework. No bundler.


Table of Contents


Architecture

Numera is a single-process Node.js HTTP server with no framework dependencies. It serves a static browser client and acts as an orchestration layer between the user and a separately-running language model. The LLM is never embedded: any OpenAI-compatible endpoint works.

Browser  (public/index.html)
  |
  |  POST /v1/chat/stream          Server-Sent Events
  |
src/router.js
  |
  +-- src/routes/chat.js
        |
        +-- src/agent/stream.js    SSE emitter, word-by-word text delivery
              |
              +-- src/agent/loop.js       Agentic loop, up to 10 iterations
                    |
                    +-- src/llm/index.js  LLMClient  (OpenAI-compatible HTTP)
                    |       |
                    |       v
                    |   LLM backend
                    |   Ollama / llama.cpp / LM Studio / OpenAI / Groq / ...
                    |
                    +-- src/tools/index.js   Tool dispatcher
                          |
                          +-- accounting.js  12 tools, reads in-memory JSON
                          +-- web.js         DuckDuckGo search + HTTP fetch
                          +-- currency.js    Fixed BCEAO exchange rates

Request lifecycle

  1. The browser sends POST /v1/chat/stream with the conversation history.
  2. agentLoop prepends the system prompt and calls LLMClient.chat(messages, TOOL_DEFINITIONS).
  3. The LLM responds with one or more tool_calls. The server executes each synchronously (accounting data, currency) or asynchronously (web, URL fetch) and appends role: "tool" messages.
  4. Steps 2–3 repeat until the LLM returns a text response with no tool_calls (maximum 10 iterations).
  5. The final text is split into ~5-token chunks and streamed to the browser at ~16 ms intervals via SSE.

Key design decisions

No streaming from the LLM. All LLM calls are non-streaming (simpler, no partial JSON accumulation). The word-by-word effect comes from chunked SSE delivery of the already-complete response.

No authentication layer. Numera is designed for local or intranet deployment. Add a reverse proxy with auth (nginx, Caddy) if public exposure is required.

Graceful tool failure. If a tool throws, the error is serialized as JSON and returned as the tool result. The LLM decides how to handle it.


Project Structure

numera/
  server.js                     Entry point. Loads data, starts HTTP server.
                                 12 lines — no business logic.
  src/
    config.js                   All configuration from environment variables.
                                 Defines 10 provider presets and auto-detection
                                 logic from URL patterns.
    data.js                     Loads and exposes comptabilite.json and
                                 plan_comptable.json as module-level singletons.
    router.js                   HTTP request router. One line per route.
    static.js                   Static file server for public/. Blocks path
                                 traversal outside publicDir.
    llm/
      index.js                  LLMClient class. Wraps POST /v1/chat/completions.
                                 Normalises Ollama native-format responses.
                                 Handles provider-specific headers.
    agent/
      prompt.js                 System prompt. Defines output language (French),
                                 currency (FCFA), chart/insight/artifact formats,
                                 and tool-use rules. Edit here to change model
                                 behaviour without touching any other file.
      loop.js                   Agentic loop. Manages the tool-call cycle.
                                 Accepts onToolCall/onToolDone hooks for the SSE
                                 emitter.
      stream.js                 SSE handler for POST /v1/chat/stream. Wraps
                                 agentLoop, drives the SSE event sequence.
    tools/
      index.js                  Registry. Aggregates DEFINITIONS and LABELS
                                 from all modules. Single execute() dispatcher.
      accounting.js             12 synchronous tools reading comptabilite.json.
      web.js                    search_web (DuckDuckGo HTML scraping) and
                                 fetch_url (HTTP/HTTPS with redirect handling).
      currency.js               convert_to_fcfa using fixed and approximate rates.
      documents.js              parseDocument: CSV (native), XLSX, DOCX, PDF,
                                 plain text, image (base64).
    routes/
      chat.js                   POST /v1/chat/stream
                                 POST /v1/chat/completions
      upload.js                 POST /v1/upload
      data.js                   GET /v1/data  /v1/plan-comptable  /v1/tools
                                 GET /v1/models  /v1/health
  public/
    index.html                  Single-page client. Tailwind CSS (CDN),
                                 Chart.js, marked.js. No build step.
  data/
    comptabilite.json           Accounting dataset. Replace to analyse a
                                 different company — no code changes needed.
    plan_comptable.json         SYSCOHADA chart of accounts.
  docker/
    Dockerfile                  Multi-stage Alpine image. Non-root user.
                                 Health check on /v1/health.
    docker-compose.yml          Three profiles: default (external LLM),
                                 ollama (Ollama local), gpu (Ollama + NVIDIA).
  docs/
    architecture.md             Detailed component diagram, SSE event protocol,
                                 provider compatibility matrix, tool extension guide.
    providers.md                Step-by-step setup for each provider.
    deployment.md               Docker commands, health check, data persistence.
  .env.example                  Full variable reference with provider examples.
  package.json                  Optional dependencies: xlsx, mammoth, pdf-parse.
  .gitignore
  .dockerignore

Features

Conversational accounting analysis Ask any question in French. The model decides which tools to invoke — no keyword routing, no hardcoded responses.

Real-time tool call transparency Each tool invocation appears in a collapsible "Reflexion" panel. The panel updates in real time as SSE events arrive. Clicking the header expands or collapses the step list.

FCFA-native output All monetary amounts are converted to FCFA using the official BCEAO fixed peg (1 EUR = 655.957 FCFA). Approximate rates are provided for USD, GBP, CHF, and MAD. Exchange rates are defined in src/config.js.

Web search and URL retrieval The model calls search_web against DuckDuckGo HTML (no API key required) and fetch_url to read specific pages. Used for sector benchmarks, OHADA/SYSCOHADA regulatory lookups, exchange rate updates, and market data.

Document ingestion Upload CSV, Excel, Word, PDF, plain text, or image files directly from the input bar. Documents are parsed server-side and injected as plain text into the conversation context. The model can analyse, compare, or query the uploaded content alongside the accounting records.

Artifact generation The model produces self-contained documents — HTML reports, Markdown summaries, CSV exports — as <artifact> blocks. Artifacts open in a slide-over panel with download support.

Inline chart rendering Responses may include <chart> blocks containing Chart.js dataset configurations. The client renders bar, line, pie, and doughnut charts directly in the conversation thread.

Insight cards <insight> blocks render as colour-coded cards (success / warning / danger / info) with a material icon, surfacing key findings without burying them in prose.

Streaming response Text is delivered in ~5-token chunks every 16 ms. The tool-call phase and the text-generation phase are visually distinct: the Reflexion panel closes automatically when the first text chunk arrives.

Conversation history The browser maintains a rolling history of up to 20 messages sent per request. Context from earlier turns is preserved across multiple questions.


Requirements

  • Node.js >= 18.0.0
  • An OpenAI-compatible LLM endpoint with function/tool calling support
Backend Tool calling Port Notes
DeepSeek Full support 3001 Default; tested with deepseek-v4-flash, deepseek-v4-pro
Ollama Model-dependent 11434 Requires llama3.1, qwen2.5, mistral-nemo, or llama3.2
llama.cpp With --jinja flag 8080 Pass --jinja to llama-server; added in build 3842
LM Studio Model-dependent 1234 Enable tool calling in Server > Chat Template settings
OpenAI Full support remote gpt-4o, gpt-4-turbo, gpt-4o-mini
Groq Full support remote llama-3.1-70b-versatile, mixtral-8x7b
Together Full support remote Llama 3.1 and 3.2 variants
Fireworks Full support remote llama-v3p1-70b-instruct and others

Installation

git clone https://github.com/yourorg/numera.git
cd numera

# Optional: enables Excel, Word, and PDF parsing
npm install

Without npm install, the server starts normally. CSV, plain text, and image uploads work natively. XLSX, DOCX, and PDF parsing return a descriptive error message if the required package is absent.


Configuration

All configuration is via environment variables. No config file is parsed at runtime.

cp .env.example .env
# Edit .env to point at your LLM endpoint
node server.js
Variable Default Description
PORT 3002 HTTP server port
LLM_PROVIDER auto-detected (see below) Provider name used for preset lookup and startup log
LLM_API_URL http://localhost:3001/v1 Full base URL including /v1
LLM_API_KEY (empty) Bearer token. Leave empty for local/unauthenticated servers
LLM_MODEL deepseek-v4-flash Model identifier sent verbatim in every request
LLM_TEMPERATURE 0.2 Sampling temperature (0.0–1.0). Lower = more deterministic
LLM_MAX_TOKENS 4096 Maximum tokens per LLM response
LLM_TIMEOUT 120000 LLM request timeout in milliseconds

Provider auto-detection

When LLM_PROVIDER is not set, the provider is inferred from LLM_API_URL:

URL pattern Detected provider
openai.com openai
anthropic.com anthropic
groq.com groq
together.xyz together
fireworks.ai fireworks
:11434 ollama
:8080 llamacpp
:1234 lmstudio
:3001 deepseek
(anything else) custom

Detection only affects the startup log and the default model. It does not change how requests are made.


Provider Setup

DeepSeek (default)

# No env vars needed if the server runs on localhost:3001
node server.js

Ollama

ollama pull llama3.1          # or: qwen2.5:7b  mistral-nemo  llama3.2

export LLM_PROVIDER=ollama
export LLM_API_URL=http://localhost:11434/v1
export LLM_MODEL=llama3.1
node server.js

Ollama has supported POST /v1/chat/completions since v0.1.24. Tool calling works with models that include a tool-capable chat template. Confirmed: llama3.1, qwen2.5, mistral-nemo, llama3.2.

llama.cpp

./llama-server \
    --model path/to/model.gguf \
    --jinja \
    --port 8080 \
    --ctx-size 8192

export LLM_PROVIDER=llamacpp
export LLM_API_URL=http://localhost:8080/v1
export LLM_MODEL=local
node server.js

--jinja enables the Jinja2 chat template engine required for tool calling. Added in build 3842 (November 2024). Recommended models: Llama 3.1 8B/70B GGUF, Qwen 2.5 7B GGUF.

LM Studio

# In LM Studio: Server > Chat Template > enable tool calling
export LLM_PROVIDER=lmstudio
export LLM_API_URL=http://localhost:1234/v1
export LLM_MODEL=local
node server.js

OpenAI

export LLM_PROVIDER=openai
export LLM_API_URL=https://api.openai.com/v1
export LLM_API_KEY=sk-...
export LLM_MODEL=gpt-4o
node server.js

Groq

export LLM_PROVIDER=groq
export LLM_API_URL=https://api.groq.com/openai/v1
export LLM_API_KEY=gsk_...
export LLM_MODEL=llama-3.1-70b-versatile
node server.js

Free tier: 14,400 requests/day, 6,000 tokens/min. Tool calling works on all available models.

Together AI

export LLM_PROVIDER=together
export LLM_API_URL=https://api.together.xyz/v1
export LLM_API_KEY=...
export LLM_MODEL=meta-llama/Llama-3.1-8B-Instruct-Turbo
node server.js

Fireworks AI

export LLM_PROVIDER=fireworks
export LLM_API_URL=https://api.fireworks.ai/inference/v1
export LLM_API_KEY=fw_...
export LLM_MODEL=accounts/fireworks/models/llama-v3p1-70b-instruct
node server.js

Usage

node server.js
# or
npm start

Open http://localhost:3002. The welcome screen displays six quick-action buttons and a text input. Press Enter or click the send button to submit a message.

Example queries

Montre le bilan complet avec graphiques
Analyse la tresorerie mensuelle et identifie les tensions de liquidite
Compare nos ratios aux benchmarks du secteur IT en Afrique de l Ouest
Genere un rapport HTML complet du compte de resultat
Exporte la liste des clients et encours en CSV
Quel est l impact d une reduction du delai client de 68 a 45 jours sur le BFR ?
Cherche les normes SYSCOHADA applicables aux immobilisations incorporelles

Attaching a document

Click the paperclip icon in the input bar. The selected file is uploaded to POST /v1/upload, parsed server-side, and appended as structured context to the next message sent. Chip indicators appear above the input showing upload status. Chips are cleared after the message is sent.

Viewing artifacts

When the model produces a report or export, a card with an "Ouvrir" button appears in the conversation. Clicking it opens the artifact in a slide-over panel. The "Telecharger" button saves the file locally.


UI Capabilities

The single-page client (public/index.html) requires no build step. It loads Tailwind CSS, Chart.js, and marked.js from CDN.

Reflexion panel

Each response begins with a collapsible "Reflexion" section. It lists each tool call in order with a status indicator:

  • Spinning icon while the tool is executing
  • Green check when the tool completes

The panel header updates to show the total number of sources consulted and collapses automatically when the first text token arrives. Click the header at any time to expand or collapse.

Artifact panel

Artifacts open in a right-side slide-over with three rendering modes:

  • HTML: rendered in a sandboxed <iframe>
  • Markdown: rendered with marked.js in a scrollable .prose container
  • CSV: rendered as a sticky-header scrollable table

Clicking the backdrop or the close button dismisses the panel.

File upload

Supported via the paperclip button. The browser reads the file, base64-encodes it, and posts JSON to /v1/upload. A chip badge shows the filename and upload result. Multiple files can be attached before sending.

Chart rendering

<chart> blocks in LLM responses are extracted before markdown parsing. Each block contains a JSON Chart.js dataset configuration. The client instantiates new Chart(canvas, { type, data, options }) 100 ms after the message is finalised. Supported types: bar, line, pie, doughnut.

Streaming display

During streaming, the assistant message renders as escaped plain text with a blinking cursor. When the done SSE event arrives, the full text is re-rendered through the complete pipeline: special-tag extraction, markdown parsing, placeholder substitution, chart initialisation.


API Reference

POST /v1/chat/stream

Primary endpoint. Returns a text/event-stream response.

Request

{
  "model": "numera-pro",
  "messages": [
    { "role": "user", "content": "Analyse le bilan" }
  ]
}

The model field is accepted but ignored server-side. The active model is configured via LLM_MODEL.

SSE event sequence

data: {"type":"start"}
data: {"type":"tool_call","name":"get_bilan","label":"Bilan comptable"}
data: {"type":"tool_done","name":"get_bilan","label":"Bilan comptable"}
data: {"type":"text_delta","content":"Voici le bilan de "}
data: {"type":"text_delta","content":"TechInnov SARL "}
data: {"type":"done","tool_count":1}

Event schema

type fields description
start — Agent loop has started
tool_call name, label Tool invocation in progress
tool_done name, label Tool execution complete
text_delta content ~5-token text fragment delivered every ~16 ms
done tool_count All text delivered, stream complete
error message Fatal error; message contains markdown explanation

Client implementation note The endpoint requires POST, so EventSource cannot be used. The browser client reads the stream with response.body.getReader() and splits on \n to parse SSE lines.


POST /v1/chat/completions

OpenAI-compatible synchronous endpoint. Runs the full agentic loop and returns a standard completion object. Suitable for non-streaming integrations, scripting, and testing.

curl -s -X POST http://localhost:3002/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{"messages":[{"role":"user","content":"CA total ?"}]}' \
  | jq .choices[0].message.content

POST /v1/upload

Parse a document server-side and return structured data.

Request body

{
  "filename": "factures_q3.csv",
  "mimeType": "text/csv",
  "content": "<base64-encoded file bytes>"
}

All fields are required. mimeType may be an empty string; format detection falls back to the file extension.

Response — CSV

{
  "type": "csv",
  "filename": "factures_q3.csv",
  "headers": ["date", "client", "montant_ht", "tva"],
  "rows": [{ "date": "2025-07-01", "client": "Acme Corp", "montant_ht": "450000", "tva": "81000" }],
  "row_count": 312
}

Response — Excel

{
  "type": "xlsx",
  "filename": "budget_2025.xlsx",
  "sheet_names": ["Produits", "Charges"],
  "sheets": {
    "Produits": [{ "Mois": "Janvier", "Budget": 65000, "Reel": 71200 }]
  }
}

Response — PDF / DOCX

{
  "type": "pdf",
  "filename": "rapport_audit.pdf",
  "pages": 14,
  "text": "Rapport d audit interne — Exercice 2025 ..."
}

Response — Image

{
  "type": "image",
  "filename": "facture_scan.jpg",
  "mimeType": "image/jpeg",
  "base64": "...",
  "size_kb": 284
}

GET /v1/health

Returns server status. Used by Docker health checks and monitoring systems.

{
  "status": "ok",
  "version": "2.0.0",
  "provider": "deepseek",
  "model": "deepseek-v4-flash",
  "data": "loaded",
  "uptime": 3842
}

data is either "loaded" or "not_loaded". The server will not accept chat requests usefully if data is "not_loaded".


GET /v1/data

Returns the full parsed comptabilite.json as JSON. Useful for inspecting the active dataset or building external integrations.

GET /v1/plan-comptable

Returns the SYSCOHADA chart of accounts from plan_comptable.json.

GET /v1/tools

Returns the complete tool definition array as sent to the LLM. Useful for debugging tool schemas and verifying that newly added tools are registered correctly.

curl -s http://localhost:3002/v1/tools | jq '[.tools[].function.name]'

GET /v1/models

Returns a minimal OpenAI-compatible model list with the active model identifier.


Tool Reference

The model has access to 15 tools, all visible in the Reflexion panel during execution.

Accounting tools — src/tools/accounting.js

All 12 tools are synchronous and read from the in-memory accounting dataset. They return raw JSON; the model formats and converts values as instructed by the system prompt.

Tool Arguments Returns
get_company_info — Name, SIRET, legal form, capital, director, fiscal year
get_bilan section? actif/passif/complet Balance sheet section(s)
get_compte_resultat — Full P&L: products, charges, net result
get_tresorerie — Monthly cash flows + net cash position
get_ratios — Gross margin, EBITDA, CAF, FR, BFR, DSO, DPO, etc.
get_clients — Top clients with revenue and outstanding receivables
get_charges_personnel — Gross salaries by position, employer contributions
get_immobilisations — Fixed assets: cost, depreciation schedule, NBV
get_budget_vs_reel — Budget vs actual with absolute and % variance
get_tva — Deductible VAT and VAT payable
get_grand_livre compte? prefix string General ledger balances, optionally filtered
get_journal_entries journal? code, limit? Journal entries (VT=sales, HA=purchases, BQ=bank, OD=misc)

Web tools — src/tools/web.js

Tool Arguments Returns
search_web query (string), max_results? (1–10) { query, results: [{ rank, url, title, snippet }] }
fetch_url url (string) { url, title, text, status, length } or { error }

search_web scrapes DuckDuckGo HTML (html.duckduckgo.com). No API key. Timeout: 12 s.

fetch_url makes a direct HTTP/HTTPS GET, strips HTML tags, and caps output at 10,000 characters. Follows up to 5 redirects. Does not execute JavaScript. Timeout: 15 s.

Currency tool — src/tools/currency.js

Tool Arguments Returns
convert_to_fcfa amount (number), from_currency? (EUR/USD/GBP/CHF/MAD) { montant_original, devise_source, montant_fcfa, taux_applique, formatted, note }

The EUR/XOF rate (655.957) is the official BCEAO fixed peg. Rates for other currencies are approximations stored in src/config.js → exchange.

Adding a tool

  1. Choose or create a module in src/tools/.
  2. Add the tool definition to DEFINITIONS using the OpenAI function schema format.
  3. Add a label to LABELS.
  4. Add the tool name to handles() (or create the function if it is a new module).
  5. Add execution logic to execute(). The function may be async.
  6. If it is a new module, import and register it in src/tools/index.js.

Tool descriptions are the primary signal the LLM uses to decide whether and when to call a tool. Write descriptions that are specific about what data the tool returns and in what situations it should be used.


Data Schema

The accounting dataset (data/comptabilite.json) follows a structure loosely aligned with SYSCOHADA conventions. Replace this file to analyse a different company. No code changes are required.

Top-level keys

Key Type Description
entreprise string Company name
siret string SIRET registration number
forme_juridique string Legal form (SARL, SA, SAS, etc.)
capital_social number Share capital in EUR
date_ouverture string Fiscal year start date (YYYY-MM-DD)
date_cloture string Fiscal year end date (YYYY-MM-DD)
dirigeant string Director name
expert_comptable string Accountant name or firm
bilan_actif object Balance sheet — assets (immobilisé, circulant)
bilan_passif object Balance sheet — equity, provisions, debt
compte_resultat object P&L — products and charges by category
ratios object Pre-computed financial ratios
tresorerie_mensuelle array 12 monthly entries: { mois, entrees, sorties, solde }
clients_principaux array Top clients: { nom, ca, secteur, encours }
immobilisations array Fixed assets: { designation, date_acquisition, valeur_acquisition, duree_amortissement, amortissement_annuel, vnc }
budget_vs_reel object { produits, charges, resultat } each with { budget, reel, ecart, ecart_pourcentage }
ecritures_journal array Journal entries: { date, piece, libelle, compte_debit, compte_credit, montant, journal }
grand_livre object Keyed by account number: { nom, solde_debut, mouvements_debit, mouvements_credit, solde_fin }

Updating the dataset at runtime

# Replace the file and restart the server
cp new_comptabilite.json data/comptabilite.json
node server.js

data.js also exports reloadData(dataDir), which can be called programmatically without restarting the process.


Document Support

Documents are uploaded via POST /v1/upload and parsed server-side. The extracted content is appended as plain text to the next chat message.

Format Extensions Requires Behaviour
CSV .csv none Auto-detects , or ;. First 200 rows returned as JSON array.
Excel .xlsx, .xls xlsx All sheets returned as arrays of objects.
Word .docx mammoth Raw text extracted, no formatting preserved.
PDF .pdf pdf-parse Text-layer PDFs only. Scanned PDFs are not supported without OCR.
Plain text .txt, .md, .log, .rst none First 50,000 characters returned.
Image .png, .jpg, .jpeg, .webp, .gif, .bmp none Returned as base64 with MIME type. Analysed by vision-capable models.

If a required package is not installed, the endpoint returns a JSON error with the install command. All other formats continue to work.

npm install              # installs all three packages
npm install xlsx         # Excel only
npm install mammoth      # Word only
npm install pdf-parse    # PDF only

Artifact System

The model produces artifacts when a standalone document is more appropriate than inline text. Artifacts are embedded in the response as <artifact> XML blocks, extracted before markdown parsing, and rendered in the slide-over panel.

Types

HTML (type="html") A complete HTML document with inline CSS. Rendered in a sandboxed <iframe> (sandbox attribute: allow-scripts allow-same-origin). The model uses a neutral colour palette: background #faf9f7, text #1a1c1b, accent #3b82f6.

Markdown (type="markdown") A structured document rendered with marked.js. Suitable for content that will be copied into external tools, email, or reporting systems.

CSV (type="csv") Tabular data rendered as a scrollable table with sticky column headers. Downloaded as a .csv file.

Triggering artifact generation

The system prompt instructs the model to produce an artifact when the user requests a report, export, or shareable document. Effective phrasings:

Genere un rapport HTML complet du bilan
Exporte les immobilisations en CSV
Produis un document Markdown resumant l exercice 2025
Cree un tableau de bord HTML autonome avec graphiques integres

Wire format

<artifact type="html" title="Rapport Bilan 2025">
<!DOCTYPE html>
<html lang="fr">
  ...
</html>
</artifact>

The client extracts <artifact> blocks using a regex before passing the response to marked.parse. Extracted blocks are stored in a module-level artifactRegistry object keyed by a generated ID. The registry is referenced when the user clicks "Ouvrir".


Docker

The Dockerfile produces a multi-stage Alpine image (~120 MB). The application runs as a non-root user (numera, uid 1001). The data/ directory should be mounted as a volume.

Build and run standalone

docker build -f docker/Dockerfile -t numera .

docker run -d \
    --name numera \
    -p 3002:3002 \
    -e LLM_API_URL=http://host.docker.internal:3001/v1 \
    -e LLM_MODEL=deepseek-v4-flash \
    -v "$(pwd)/data":/app/data:ro \
    --restart unless-stopped \
    numera

host.docker.internal resolves to the host machine on Docker Desktop (macOS, Windows) and when extra_hosts: host-gateway is set (Linux, as in the compose file).

Compose — external LLM

cp .env.example .env
# Set LLM_API_URL, LLM_API_KEY, LLM_MODEL in .env
docker compose up -d
docker compose logs -f numera

Compose — Ollama (CPU)

docker compose --profile ollama up -d
docker exec ollama ollama pull llama3.1
# Numera becomes available at http://localhost:3002

The ollama service has a health check on ollama list. The numera-ollama service starts only after Ollama passes the health check.

Compose — Ollama with NVIDIA GPU

# Host requirement: nvidia-container-toolkit installed and configured
docker compose --profile gpu up -d
docker exec ollama ollama pull llama3.1

Health check

curl http://localhost:3002/v1/health
# {"status":"ok","version":"2.0.0","provider":"ollama","model":"llama3.1","data":"loaded","uptime":120}

Contributing

Reporting issues

Include:

  • The exact question or action that triggered the problem
  • The full server startup output (node server.js output before the first request)
  • LLM provider, model, and version
  • Node.js version (node --version)

Adding a provider

Any endpoint implementing POST /v1/chat/completions per the OpenAI specification works without code changes. To register a named preset:

  1. Add an entry to the PROVIDERS map in src/config.js with baseUrl, model, and requiresKey.
  2. Add a URL pattern match in detectProvider() if auto-detection from URL is desired.
  3. Add a setup section to docs/providers.md.

Modifying the system prompt

src/agent/prompt.js is the single point of control for model behaviour: output language, output currency, chart format, insight format, artifact format, and tool-use rules. Changes take effect after server restart. The file is intentionally isolated from all other modules so it can be edited without risk of breaking application logic.

Current enforced behaviours:

  • French as the output language
  • FCFA as the mandatory output currency with explicit conversion instruction
  • Prohibition on fabricated numbers
  • <chart>, <insight>, and <artifact> output formats

Adding a document format

Extend parseDocument() in src/tools/documents.js. The function signature is async parseDocument(filename, mimeType, buffer). Return a plain JSON-serialisable object. The object is serialised and appended verbatim to the conversation context, so keep it concise and structured.

Replacing the accounting dataset

Replace data/comptabilite.json with a file matching the schema documented in the Data Schema section. All 12 accounting tools read exclusively from this file. No code changes are required.


Further Reading

  • docs/architecture.md — detailed component diagram, full SSE event protocol, provider compatibility matrix, tool extension walkthrough
  • docs/providers.md — step-by-step setup instructions for each supported provider
  • docs/deployment.md — Docker commands, health check integration, data persistence patterns
  • .env.example — annotated variable reference with complete provider configuration examples

License

MIT. See LICENSE.

About

Numera is a self-hosted accounting intelligence system powered by an LLM agentic loop. It connects any OpenAI-compatible model to structured financial data, live web search, and user-uploaded documents (CSV, XLSX, DOCX, PDF, images). All responses are in French with FCFA amounts purpose-built for businesses under UEMOA, CEMAC, and SYSCOHADA juri

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages