Portfolio-grade AI engineering project for procurement quotation analysis.
Upload supplier quotations in PDF, DOCX, TXT or XLSX and the system runs a controlled pipeline:
Ingest → Extract → Validate → Deterministically Score → Compare → Recommend → Show Evidence
The project deliberately does not make the LLM the sole decision-maker. Numeric scoring and completeness checks are deterministic; the LLM is used for document extraction and cautious reasoning over the resulting evidence.
- 🧠 LLM provider abstraction: provider abstraction with cloud-first
automode and optional Ollama for local/private inference. - 📋 Structured extraction: quotation fields are normalized into JSON.
- 🧮 Deterministic scoring: cost and completeness are calculated in Python, reducing LLM-driven ranking errors.
- 🔎 Evidence traceability: source snippets/pages/sheets are retained alongside extraction results.
⚠️ Completeness validation: missing fields are explicitly surfaced before a recommendation.- 📊 Streamlit dashboard: overview, quotation table, scoring, validation, recommendation, evidence and raw output.
- 🚀 FastAPI service: production-shaped
/health,/health/llm,/analyzeendpoints and Swagger docs. - 🐳 Docker Compose: lightweight API + Streamlit base stack; Ollama is an explicit optional overlay with persistent model storage.
- 🧪 Automated tests: core ingestion, scoring, JSON parsing and API health tests.
- 🔐 Secret-safe configuration:
.envis ignored and credentials are environment-driven. - 🌐 Optional external research: Tavily is disabled unless explicitly configured; no mock market data is generated.
- 🎯 Deployment-friendly: all core services are containerized; Ollama can use CPU or an optional NVIDIA GPU override.
Browser
│
▼
Streamlit
│
▼
Quotation pipeline
│
├── deterministic validation/scoring
└── LLM provider abstraction
├── LiteLLM
└── optional Ollama
Browser
│
▼
Streamlit on Render
│
▼
Quotation pipeline
│
└── OpenAI-compatible HTTPS LLM endpoint
The FastAPI service remains part of the repository for API demos and local development, while the first Render deployment uses Streamlit directly to keep the public demo lightweight.
A common portfolio weakness is asking an LLM to extract data and then blindly choose the cheapest or "best" result. This project separates those responsibilities:
- LLM: extraction and qualitative reasoning.
- Python: completeness checks and measurable cost scoring.
- Human: final procurement decision.
The UI explicitly labels the score so an interviewer can see how it was calculated.
PDF pages, DOCX paragraphs, text documents and XLSX sheets are represented as evidence blocks. The UI exposes these snippets so extracted fields can be investigated rather than treated as unexplained AI output.
The repository includes both a browser-facing Streamlit application and a FastAPI backend. The API provides health checks and automatic OpenAPI/Swagger documentation.
docker-compose.yml starts:
- Ollama — local model runtime.
- ollama-init — pulls the configured model once.
- API — FastAPI backend.
- Streamlit — user interface.
The Ollama model is persisted in a named Docker volume, so it is not downloaded on every restart.
The base stack intentionally does not start Ollama. This keeps cloud deployment lightweight and avoids downloading a local model when a hosted LLM is configured.
cp .env.example .env
# Set GEMINI_API_KEY or OPENAI_API_KEY, then:
docker compose up --buildOpen:
- Streamlit:
http://localhost:8501 - API:
http://localhost:8000 - Swagger:
http://localhost:8000/docs
If you want fully local/private inference:
docker compose -f docker-compose.yml -f docker-compose.ollama.yml up --buildThe Ollama model is persisted in a named volume. The first startup downloads the configured model.
With Docker + NVIDIA Container Toolkit:
docker compose -f docker-compose.yml -f docker-compose.ollama.yml -f docker-compose.gpu.yml up --buildOllama is optional rather than a hard dependency of the application.
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .envFor a local Ollama installation, change:
OLLAMA_BASE_URL=http://localhost:11434Then:
ollama pull qwen3:1.7b
streamlit run streamlit_app.pyAPI:
uvicorn app:app --reload --port 8000pytest -qTests intentionally include an offline path so CI does not require an LLM API key.
python agent_interface.py \
--document sample_data/sample_quotations.txt \
--query "Compare quotations and recommend the strongest option"curl -X POST http://localhost:8000/analyze \
-F "file=@sample_data/sample_quotations.txt" \
-F "query=Compare the quotations and identify risks" \
-F "criteria=cost,completeness,timeline,terms".
├── app.py # FastAPI service
├── streamlit_app.py # Streamlit UI
├── launcher.py # Local launcher
├── agent_interface.py # CLI
├── config.py # Environment configuration/prompts
├── modules/
│ ├── ai_agent.py # Orchestration
│ ├── document_processor.py # Multi-format ingestion + evidence
│ ├── llm_provider.py # Ollama/OpenAI/Gemini abstraction
│ ├── scoring.py # Deterministic scoring/validation
│ └── web_search.py # Optional Tavily integration
├── tests/
│ ├── test_core.py
│ └── test_api.py
├── sample_data/
│ └── sample_quotations.txt
├── Dockerfile
├── docker-compose.yml
├── docker-compose.gpu.yml
├── .github/workflows/ci.yml
└── requirements.txt
- Scanned PDFs are not OCR'd yet; image-only PDFs need OCR before extraction.
- Evidence blocks currently provide source locations/snippets, but not field-level bounding boxes.
- Qualitative criteria such as quality/reputation are not fabricated into numeric scores.
- Shared public deployment should add authentication, rate limiting and persistent audit storage.
- Procurement recommendations should be reviewed by a human before contractual decisions.
- Upload two quotations.
- Show the extracted structured fields.
- Open Validation and point out missing information.
- Open Scoring and explain why cost/completeness are deterministic.
- Open Evidence to show traceability.
- Show the recommendation and explain that the LLM reasons over evidence rather than inventing facts.
- Open
/docsto demonstrate the API boundary. - Explain Docker Compose and the persistent Ollama model volume.
This is a portfolio/decision-support system. It is not financial, legal, tax or procurement advice.
The extraction layer is deliberately tolerant of provider/model differences. Before domain validation it normalizes common JSON shapes (wrapped objects, bare arrays and single records), nested scalar objects, and localized monetary strings into a canonical Pydantic model. This prevents provider-specific formatting from leaking into business logic and avoids supplier/document-specific hardcoding.
Completed analysis results are stored independently of the current file uploader selection. Removing all files from the uploader does not erase the last completed analysis. The dashboard labels those results as the last completed analysis and provides an explicit Clear previous results action. If a different set of files is selected, the previous results remain visible but are clearly marked as not yet analyzed until the user runs a new analysis.