CoolFace
Apppublic

yuan-de/chat-LCA

sourceHugging Faceupdated 2mo agoView on Hugging Face
0likes
App README

LCA ChatBot

A Retrieval-Augmented Generation (RAG) chatbot powered by Claude for answering questions about Life Cycle Assessment (LCA) concepts and methodologies, grounded in the Hauschild et al. textbook "LCA: Theory and Practice".

Overview

LCA ChatBot provides an intelligent conversational interface for exploring LCA concepts. The system combines vector embeddings for semantic search with Claude AI for context-aware answers, ensuring responses are grounded in the source material.

Key capabilities:

  • —Chat interface with natural language questions
  • —Source citations showing chapter and page references
  • —Conversation history for contextual follow-up questions
  • —RAG pipeline combining retrieval and generation
  • —MMR (Maximum Marginal Relevance) for diverse, relevant results
  • —Hybrid GraphRAG: a knowledge graph of LCA concepts augments vector retrieval
  • —Runtime Retrieval-mode toggle (RAG only vs RAG + Knowledge graph) on both the chat and Report Generator pages

Architecture

The system operates in two distinct phases:

Phase A: Ingestion (Offline)

PDF (Textbook) 
    -> Extract text
    -> Clean and chunk (overlapping segments)
    -> Embed with bge-large-en-v1.5
    -> Store in Chroma vector database

Runs once to build the knowledge base. Output is persisted for Phase B.

Phase B: Query (Online)

User Question
    -> Retrieve top-10 MMR chunks
    -> Format context with source tags
    -> Build prompt (system + context + question)
    -> Call Claude API
    -> Extract answer and sources
    -> Display results

Runs for each user query. Responses include citations.

A one-time offline Phase A2 builds a knowledge graph from the same chunks, which Phase B fuses into retrieval (hybrid GraphRAG — see the dedicated section below). It is bypassable via USE_GRAPH, so the chatbot degrades cleanly to pure-vector RAG when the graph is off or absent.

Requirements

  • —Python 3.10 or higher
  • —Virtual environment (recommended)
  • —Anthropic API key (https://console.anthropic.com/)
  • —2GB+ disk space (for embedding model)

Installation

1. Set up Python environment

bash
cd "c:\Users\user\My Project\LCA-ChatBot"
python -m venv .venv
.\.venv\Scripts\Activate.ps1  # Windows PowerShell
# or: source .venv/bin/activate  # macOS/Linux

2. Install dependencies

bash
pip install -r requirements.txt

3. Configure API key

Create .env file in project root:

ANTHROPIC_API_KEY=sk-ant-xxxxx

Keep this file private. Do not commit to version control (included in .gitignore).

4. Build vector database (first time only)

bash
python -m phase_a_ingestion.build_index

This process:

  • —Extracts text from every PDF registered in config.BOOKS (under data/raw/)
  • —Chunks text with configurable size and overlap
  • —Generates embeddings (downloads ~1.3GB model on first run)
  • —Stores vectors in chroma_db/

Expected time: 5-10 minutes per book on CPU.

5. Start chatbot

bash
streamlit run app/streamlit_app.py

Opens at http://localhost:8501

Usage

Type questions naturally in the chat interface. Examples:

  • —What is a functional unit in LCA?
  • —What are the main phases of LCA?
  • —How is environmental impact calculated?
  • —What is the difference between cradle-to-grave and cradle-to-cradle?

Each answer includes source citations in the format "Chapter Name, p.XX".

Retrieval mode. The sidebar has a Retrieval mode radio to switch between RAG only (pure vector retrieval) and RAG + Knowledge graph (vector retrieval plus LCA graph relationship facts). It defaults to config.USE_GRAPH and only appears when the graph artifacts under graph/ exist; when the graph is off, the answer is pure-vector RAG and no graph expander is shown. The same toggle is available on the Report Generator page.

Report Generator (PDF → fixed-template carbon footprint report)

The app is multipage. Besides the chat, a Report Generator page (sidebar nav, or the "📄 Report Generator" link on the chat page) turns an uploaded LCA study into a Product Carbon Footprint Study Report that follows the paper's fixed ISO 14067:2018 template (Appendix D).

How it works:

  1. 1.Upload a PDF (an LCA study or article), pick a model, and choose a Retrieval mode (RAG only vs RAG + Knowledge graph — same sidebar toggle as the chat page).
  2. 2.The PDF is extracted, cleaned, and chunked with the same Phase A pipeline, then embedded into an ephemeral, in-memory vector store (it never enters chroma_db/).
  3. 3.A set of per-section retrieval queries (functional unit, system boundary, data sources, carbon footprint by life cycle stage, etc.) pull the most relevant passages via MMR.
  4. 4.If RAG + Knowledge graph is selected, the LCA concept relationships linked from those section queries are prepended to the study context as an extra grounding block (the graph is built from the reference library, so it supplies canonical LCA domain structure, not facts from the uploaded PDF). The graph connections used are shown in a "🕸️ Knowledge graph" expander.
  5. 5.A single LLM call fills the fixed template from those passages, marking any field the study doesn't cover as `[Information not provided]` rather than inventing it.
  6. 6.The report renders as Markdown with download buttons for both `.md` and `.docx` (the Word file is built from the Markdown by phase_c_report/docx_export.py); if the study contains per-stage carbon numbers, a pie chart of the stage shares is shown.

This adapts the paper's third pillar. The paper generated reports via a code-interpreter agent reading a CSV of LCIA results; here the input is an arbitrary PDF processed by RAG, so the carbon-by-stage table is filled only from numbers the study actually contains. Relevant settings live in config.py: REPORT_MAX_TOKENS (larger output cap than the chat's LLM_MAX_TOKENS) and REPORT_TOP_K (chunks per section query). The pipeline is in phase_c_report/ and the page is app/pages/1_Report_Generator.py.

Deployment (Hugging Face Spaces)

The chatbot is hosted live on Hugging Face Spaces: https://huggingface.co/spaces/sshobhit1106/LCA-ChatBot-Streamlit

The Space runs the Docker SDK (HF retired the standalone Streamlit SDK), so Streamlit is launched from a `Dockerfile` rather than auto-detected.

How it works

  • —Space config lives in the README front matter. The YAML block at the top of this file (sdk: docker, app_port: 7860, title, emoji, colors) is read by HF to configure the Space.
  • —Container. `Dockerfile` builds on python:3.11-slim, installs requirements.txt, copies the app, and starts Streamlit on port 7860 (the port HF Spaces expects). It runs as a non-root user (UID 1000, the HF convention) with a writable HOME so the bge-large embedding model can cache under ~/.cache/huggingface on first boot. CORS/XSRF are disabled because the Space is served inside an iframe.
  • —Vector store ships in the image. The pre-built chroma_db/ is committed to the repo so no ingestion runs on the Space — the chatbot is query-ready at boot. Because HF rejects large non-LFS blobs (>10 MB), the Chroma binaries (*.bin, *.sqlite3, *.pickle) are tracked via Git LFS — see `.gitattributes`.
  • —API key. ANTHROPIC_API_KEY is not committed; it is set as a Space secret in the HF Space settings and read from the environment at runtime (same variable name as the local .env).

Deploying an update

The HF Space is wired up as a git remote named space. Push to it to redeploy:

bash
git push space main

HF rebuilds the Docker image and restarts the Space automatically. To re-add the remote on a fresh clone:

bash
git remote add space https://huggingface.co/spaces/sshobhit1106/LCA-ChatBot-Streamlit

If you rebuild chroma_db/ locally, commit the regenerated LFS-tracked binaries before pushing so the Space serves the updated index.

Project Structure

LCA-ChatBot/
├── phase_a_ingestion/          Offline indexing pipeline
│   ├── build_index.py          Main ingestion script
│   ├── extract.py              PDF text extraction
│   ├── clean.py                Text cleaning
│   ├── chunk.py                Text segmentation
│   └── embed.py                Embedding model setup
├── phase_a2_graph/             Offline knowledge-graph build (GraphRAG)
│   ├── ontology.py             Closed node/relation vocabulary + aliases
│   ├── extract_graph.py        LLM triple extraction per chunk
│   └── build_graph.py          Canonicalize + assemble networkx graph
├── phase_b_query/              Online query pipeline
│   ├── rag_pipeline.py         Main orchestrator (vector + graph fusion)
│   ├── retriever.py            Vector search
│   ├── graph_store.py          Graph link + traverse + format
│   ├── prompt.py               Prompt template
│   └── llm.py                  LLM setup
├── app/                        User interface
│   └── streamlit_app.py        Chat UI
├── evaluation/                 System evaluation
│   ├── generate_qa.py          Generate test Q&A pairs (single-chunk)
│   ├── generate_multihop_qa.py Generate relational 2-hop Q&A pairs (graph-seeded)
│   ├── metrics.py              Evaluation metrics
│   └── run_eval.py             3-way: no-RAG vs RAG vs RAG+graph
├── data/
│   ├── raw/                    Input data
│   └── processed/              Output artifacts
├── chroma_db/                  Vector database (generated, shipped via Git LFS)
├── graph/                      Knowledge-graph artifacts (pkl + node embeddings)
├── config.py                   Global configuration
├── requirements.txt            Dependencies
├── Dockerfile                  Hugging Face Spaces (Docker SDK) image
├── .gitattributes              Git LFS rules for chroma_db/ binaries
├── .env                        API keys (not in git)
└── README.md                   This file (also holds the HF Space config)

Configuration

Edit config.py to customize system behavior:

ParameterDefaultPurpose
CHUNK_SIZE1000Characters per chunk
CHUNK_OVERLAP200Character overlap between chunks
EMBEDDING_MODELBAAI/bge-large-en-v1.5Embedding model identifier
TOP_K10Number of chunks retrieved per query
FETCH_K30Candidates evaluated before re-ranking
MMR_LAMBDA0.5Relevance vs diversity balance (0=diversity, 1=relevance)
LLM_PROVIDERanthropicLLM provider
LLM_MODELclaude-opus-4-8Claude model version
LLMMAXTOKENS1024Maximum answer length
SEARCH_TYPEmmrSearch strategy (Maximum Marginal Relevance)
USE_GRAPHTrueMaster on/off switch for hybrid graph retrieval
GRAPHEXTRACTMODELclaude-haiku-4-5Cheap model for bulk offline triple extraction
GRAPHMERGETHRESHOLD0.92Offline: cosine above which two node names are one entity
GRAPHLINKTHRESHOLD0.55Online: min cosine to link a question to a graph node
GRAPH_HOPS1Traversal depth from each linked entity
GRAPHMAXENTITIES5Max question entities linked into the graph
GRAPHMAXFACTS30Max relationship facts injected into the prompt

Development

Rebuild vector database

bash
python -m phase_a_ingestion.build_index

Deletes the existing database and rebuilds the index from every PDF in config.BOOKS. Use when changing CHUNK_SIZE, the embedding model, or replacing a source PDF.

Add a new source without a full rebuild

To register and index an additional PDF without re-embedding everything:

  1. 1.Drop the PDF in data/raw/ and add an entry to BOOKS in config.py (file, a short book id, and a source citation label).
  2. 2.Embed only the new file and append it to the existing store:
bash
python -m phase_a_ingestion.build_index --add YourFile.pdf

This embeds only the new book's chunks (seconds, not minutes), leaves the other books untouched, and skips any book already present in the store.

Run evaluation

bash
python -m evaluation.generate_qa --num-chunks 50 --pairs-per-chunk 5
python -m evaluation.run_eval --limit 240

generate_qa samples book chunks and asks Claude to write self-contained Q&A pairs (≈ num-chunks × pairs-per-chunk total), appended to data/processed/qa_pairs.jsonl. run_eval then answers each pair under three conditions and scores all three:

  1. 1.no-RAG — the model alone, no context (baseline)
  2. 2.RAG — vector retrieval only, knowledge graph off
  3. 3.RAG+graph — hybrid GraphRAG: vector retrieval plus graph relationship facts

The same model produces all three so each comparison is isolated: RAG vs no-RAG measures retrieval; RAG+graph vs RAG measures the graph's incremental gain. Following the paper, BERTScore and ROUGE-L are reported as recall (paper Eq. 4 and Eq. 7 — the values the paper's 0.85 figure uses), alongside cosine similarity. Each eval_predictions.jsonl record now carries baseline, rag, and rag_graph; pairs cached before the graph existed keep their baseline/rag and only the missing rag_graph answer is generated on the next run.

Two shared output files (everything is tagged inside, not split across filenames):

FileOne row perTagged with
data/processed/eval_predictions.jsonlanswered pairmodel, provider, book, question
data/processed/eval_results.jsonmodel + book-filter runmodel, provider, books, scores

run_eval resumes by (model, question): a pair already answered by a given model is skipped, so re-running re-scores from cache with no new LLM calls. A results row is upserted — re-running the same model+book-filter replaces its row in place. Note eval_predictions.jsonl holds one record per (pair × model evaluated), so its count is the number of answers run — not the number of QA pairs (a book you haven't evaluated contributes zero prediction records).

Choosing the model (and provider)

By default run_eval uses config.LLM_PROVIDER / config.LLM_MODEL. Override per run with --model (provider is inferred: gpt*/o* → openai, claude* → anthropic; pass --provider if it can't be inferred):

bash
python -m evaluation.run_eval --books finkbeiner --model claude-sonnet-4-6
python -m evaluation.run_eval --books finkbeiner --model gpt-5.4

QA generation is pinned separately to config.QA_GEN_MODEL (claude-sonnet-4-6) so the test set stays fixed no matter which model you grade.

Generate Q&A for specific books only

When you add new PDFs to config.BOOKS, use --books to generate pairs for just those books without regenerating the ones already covered (new pairs are appended, not overwritten):

bash
python -m evaluation.generate_qa --books ilcd guinee finkbeiner --num-chunks 50 --pairs-per-chunk 5

Pass one or more book ids from config.BOOKS. Unknown ids fail loudly. Omit --books to sample across all registered books.

Evaluate specific books only

run_eval takes the same --books filter to score only the pairs tagged with the given book id(s). Results land in the shared files, tagged by book/model inside each record:

bash
python -m evaluation.run_eval --books ilcd            # one book
python -m evaluation.run_eval --books ilcd guinee     # a subset

Only pairs that carry a matching book id are selectable. If a requested book has no pairs yet, run_eval stops and points you at generate_qa --books. Omit --books to score across all pairs.

Evaluate a random sample

--limit N always takes the first N pairs (deterministic but book-skewed — the first pairs are all hauschild). To score a random subset instead, use --sample N, which draws N pairs at random after any --books filter:

bash
python -m evaluation.run_eval --sample 50            # random 50 of the 770 pairs
python -m evaluation.run_eval --sample 50 --seed 7   # a different reproducible draw
python -m evaluation.run_eval --sample 50 --books hauschild   # random 50 from one book

The draw is reproducible: --seed (default 42) fixes which pairs are picked, so repeat runs grade the same sample; change the seed for a fresh draw. As with every run, answers are resumed from cache by (model, question), so sampling a fully-cached model (e.g. gpt-5.4) makes no LLM calls — it just re-scores.

A sampled run is print-only: it does not upsert eval_results.json. This is deliberate — a random subset shares the (model, books) key of the model's real full-coverage row, so saving it would silently overwrite the canonical 770-pair numbers. (New answers are still checkpointed to eval_predictions.jsonl, which is keyed by question and safe to append to.) Use --sample for a quick spot-check; use --limit/--books (which do save) for the numbers of record.

Relational / multi-hop subset (testing the graph)

The default QA set draws each pair from one chunk, so its answers live inside a single retrieved passage — the ideal case for pure vector RAG, where the knowledge graph has little to add. To probe the scenario the graph targets — questions that require connecting two facts — build a relational subset with generate_multihop_qa, then score it with run_eval --qa-file:

bash
python -m evaluation.generate_multihop_qa --num 50          # -> data/processed/qa_multihop.jsonl
python -m evaluation.run_eval --qa-file data/processed/qa_multihop.jsonl

generate_multihop_qa walks the knowledge graph for 2-hop paths A →r1→ B →r2→ C (using only specific relations, skipping the generic related_to/has_property and mega-hub bridges), then grounds a question + reference answer in the actual source passages those edges came from — never in the graph triples, so RAG and RAG+graph are graded on equal footing (no circular advantage for the graph). It appends to qa_multihop.jsonl; delete the file first for a clean regenerate. Each pair records the 2-hop path it was built from.

run_eval --qa-file PATH scores any JSONL under the same 3-way comparison. Like --sample, it is print-only and ignores --books; answers are still cached by (model, question). In practice the graph's KG−RAG gap flips from slightly negative on the general set to consistently positive on this relational set — confirming the graph helps most on multi-hop questions — though the margins are small when the questions are also answerable from the model's own knowledge.

Merged (cross-book) result per model

--merge re-scores a model's cached predictions across all books at once and upserts a single row (with every book listed by name in books). It makes no LLM calls — it only re-runs the metrics on answers already in eval_predictions.jsonl:

bash
python -m evaluation.run_eval --merge                  # every fully-covered model
python -m evaluation.run_eval --merge --model gpt-5.4  # one model

A merge is only allowed when the model has fully evaluated every book that has QA pairs (its prediction count per book matches that book's pair count). If a book is missing or only partially answered, --merge refuses and names the gap:

Cannot merge — gpt-5.4: not fully evaluated — missing hauschild (0/250), ilcd (0/240).
Evaluate the remaining book(s) first, e.g.
  python -m evaluation.run_eval --model gpt-5.4 --books hauschild ilcd

With --model, an incomplete model is a hard error; without it, incomplete models are skipped (with a reason) and only fully-covered ones are merged. Books that have no QA pairs (e.g. a freshly added PDF) are not part of the coverage requirement. Typical workflow: run the per-book evals you want, then run --merge once to refresh the cross-book rows.

How the merge is computed. It does not average the per-book scores. It re-pools every cached prediction across all books into one list and recomputes each metric from scratch over that combined set — i.e. the mean over all N pairs (N = total pairs across the model's books):

merged_metric = (1/N) * Σ  score(prediction_i, reference_i)

Because the average is taken at the pair level, the merged value is a pair-count-weighted (micro) average of the per-book scores:

merged = Σ_b ( n_b · score_b ) / Σ_b n_b

where n_b is the number of pairs in book b and score_b is that book's mean. So larger books (e.g. hauschild at 250 pairs) pull the merged number more than small ones (finkbeiner at 40). This is micro-averaging, not a plain mean of the per-book numbers (macro-averaging). The three metrics aggregate this way: BERTScore recall, cosine, and ROUGE-L recall are each averaged over all N pooled pairs (see `evaluation/metrics.py`).

Code quality checks

bash
pylint phase_a_ingestion phase_b_query app
mypy --strict .

Evaluation Results

Full results from data/processed/eval_results.json. RAG is compared against an ungrounded no-retrieval baseline (higher is better). RAG+Graph adds the hybrid knowledge-graph layer on top of vector retrieval. All BERTScore values are recall.

ModelProviderBooksnMetricRAGRAG+GraphNo-RAG
claude-sonnet-4-6anthropicall 4 books770BERTScore (R)0.9210.9180.880
Cosine0.7450.7390.689
ROUGE-L0.7600.7850.528
gpt-5.4openaiall 4 books770BERTScore (R)0.9260.9240.886
Cosine0.7630.7550.706
ROUGE-L0.6900.7230.492

RAG outperforms the no-RAG baseline on every metric for both models. Both models clear the pre-set target of BERTScore recall ≥ 0.80 with RAG (0.921 and 0.926). RAG+Graph delivers an additional lift on ROUGE-L for claude-sonnet-4-6 (0.785 vs 0.760) and gpt-5.4 (0.723 vs 0.690), with BERTScore and cosine within rounding of pure-vector RAG.

How RAG Works

Retrieval-Augmented Generation combines three steps:

  1. 1.Retrieval: Vector similarity search finds passages semantically related to the question
  2. 2.Augmentation: Relevant passages are inserted into the prompt as grounding context
  3. 3.Generation: LLM reads context and generates an informed response

This approach constrains answers to the source material, reducing hallucinations and providing verifiable citations.

Knowledge Graph (Hybrid GraphRAG)

On top of pure vector RAG, the chatbot layers a knowledge graph of LCA concepts and their relationships. Vector search retrieves passages that resemble the question; a graph encodes how concepts connect (ReCiPe —covers→ Global Warming, CO₂ —contributes_to→ Global Warming), which helps relational, multi-hop questions whose answer is spread across chunks that individually don't match the query. The two retrievers run together and the graph facts are prepended to the vector context — a strict augmentation, never a replacement.

The whole path is gated by USE_GRAPH in config.py. When it's off, or the graph artifacts under graph/ are missing, the system falls back silently to pure-vector RAG (graph_store.is_available() → False), so it can never regress below the baseline.

Phase A2 — building the graph (offline, one-time)

Built from the same data/processed/chunks.jsonl that Phase A embeds, in phase_a2_graph/:

  1. 1.Closed ontology (ontology.py) — a small, fixed LCA vocabulary the extractor MUST draw from. The closed set stops the extractor inventing inconsistent labels ("GWP" vs "Global Warming Potential") that fragment the graph. Alongside it is a hand-seeded alias map.

10 node types:

TypeCovers
MethodologyPhaseGoal Definition, Scope, LCI, LCIA, Interpretation
LifeCycleStageRaw Material Acquisition, Production, Use, End of Life
ImpactCategoryGlobal Warming, Acidification, Eutrophication, …
LCIAMethodReCiPe, CML, TRACI, ILCD, …
Conceptfunctional unit, system boundary, allocation, cut-off
Materialsteel, concrete, biofuel, …
Processmanufacturing, transport, incineration, …
FlowCO₂, CH₄, energy, water (emissions / resource flows)
StandardISO 14040, ISO 14044, ISO 14067
Sectorbuildings, food, electromobility, …

10 relation types:

RelationExample
part_ofProduction part_of Life Cycle
precedesRaw Material Acquisition precedes Production
emitsProcess emits Flow (CO₂)
consumesProcess consumes Material / energy
contributes_toCO₂ contributes_to Global Warming
measured_byImpactCategory measured_by characterization factor / GWP
coversLCIAMethod covers ImpactCategory
defined_inConcept / stage defined_in Standard (ISO 14044)
has_propertygeneric attribute link
related_tofallback association when no specific relation fits
  1. 1.Triple extraction (extract_graph.py) — each chunk is sent to Claude Haiku (cheap, pinned via GRAPH_EXTRACT_MODEL) with a JSON prompt that returns only on-ontology {subject, relation, object} triples grounded in that chunk. Parsing and validation are defensive; anything malformed or off-ontology is dropped, and errors never abort the run.
  2. 2.Canonicalization + assembly (build_graph.py) — apply the alias map, then merge near-duplicate names by BGE embedding cosine ≥ `GRAPH_MERGE_THRESHOLD` (0.92) so "system boundaries" == "system boundary". The result is a networkx.MultiDiGraph: nodes carry a type, an occurrence count, and provenance citations; edges carry a weight (how often the fact was asserted → confidence) and citations. Extraction is checkpointed to data/processed/graph_triples.jsonl, so --resume continues after any stop.

Three artifacts are persisted to graph/ (committed like chroma_db/): lca_graph.pkl, node_embeddings.npz, node_index.json. The current graph holds ~18.3k nodes and ~36k edges over the four books.

bash
python -m phase_a2_graph.build_graph                 # full corpus
python -m phase_a2_graph.build_graph --resume        # continue after a stop
python -m phase_a2_graph.build_graph --graph-only    # rebuild from checkpoint, no LLM calls

Phase B — using the graph (per question)

In phase_b_query/graph_store.py, rag_pipeline.answer():

  1. 1.Links the question to graph nodes — embed it with the same BGE model, keep nodes with cosine ≥ GRAPH_LINK_THRESHOLD (0.55), up to GRAPH_MAX_ENTITIES (5).
  2. 2.Traverses their neighborhood — BFS out to GRAPH_HOPS (1) in both directions, rank incident facts by edge weight, cap at GRAPH_MAX_FACTS (30).
  3. 3.Formats the facts as compact one-liners and prepends them to the retrieved chunks before the LLM call.

The answer returns the graph_facts it used, shown in the chat under a collapsible "🕸️ Knowledge graph (N connections)" expander.

Per-call toggle (UI + code). answer(..., use_graph=None|True|False) and generate_report(..., use_graph=...) override the graph for a single call: None follows config.USE_GRAPH, True/False force it on/off. The Retrieval mode radio on both the chat and Report Generator pages drives this override, so users can compare RAG vs RAG+graph live without restarting. The radio is shown only when graph_store.has_artifacts() is true (the graph was built); otherwise both pages run pure-vector RAG. The report path uses graph_store.graph_context_multi(SECTION_QUERIES, ...), which pools the entities linked by every section query into one traversal.

Limitations

  • —Text only — no visuals. Ingestion extracts only the text layer of each PDF (PyMuPDF get_text), and the embedding model (bge-large-en-v1.5) is text-only. Images, figures, diagrams, charts, and equations are not read or indexed. A figure's caption is captured (it is text on the page), but the figure's visual content is not, so the chatbot cannot answer questions that depend on interpreting a diagram or chart.
  • —Tables are flattened. Tables are captured only as the raw text PyMuPDF reads in reading order, which can be messy or lose structure.
  • —Scanned / image-only pages are skipped. Pages with no text layer (e.g. scanned documents) produce empty text and are dropped during cleaning, contributing nothing to retrieval.

Dependencies

Core dependencies:

  • —langchain, langchain-community, langchain-text-splitters: LLM orchestration
  • —chromadb, langchain-chroma: Vector database
  • —sentence-transformers, langchain-huggingface: Embeddings (bge-large-en-v1.5)
  • —langchain-anthropic: Claude API integration
  • —streamlit: Web UI
  • —pymupdf, pdfplumber: PDF extraction

Evaluation:

  • —bert-score, rouge-score: Evaluation metrics
  • —scikit-learn: Statistical analysis
  • —matplotlib: Visualization

Utilities:

  • —python-dotenv: Environment variable loading
  • —tqdm: Progress bars

See requirements.txt for complete list. Versions are unpinned; freeze with pip freeze > requirements.lock.txt after validation.

Citation

This project builds on the Hauschild et al. textbook:

Hauschild, Michael Z., Ralph K. Rosenbaum, and Stig Irving Olsen. 
"LCA: Theory and Practice." Springer, 2018.

Troubleshooting

ModuleNotFoundError when running scripts

  • —Verify virtual environment is activated
  • —Run from project root directory
  • —Check sys.path.insert in app/streamlit_app.py

ANTHROPIC_API_KEY not found

  • —Create .env file in project root
  • —Add ANTHROPICAPIKEY=sk-ant-xxxxx
  • —Ensure .env is in .gitignore

Slow first run

  • —Embedding model downloads on first use (~1.3GB)
  • —CPU inference is intentionally slow
  • —Model is cached after first run

Vector database errors

  • —Delete chroma_db/ directory
  • —Rebuild with: python -m phaseaingestion.build_index

Streamlit not found

  • —Run: pip install -r requirements.txt
  • —Verify requirements.txt is current

License

This project uses copyrighted material (Hauschild et al. textbook) for educational purposes. Refer to the textbook's usage terms.

Support

For issues:

  1. 1.Review troubleshooting section above
  2. 2.Check config.py settings match your environment
  3. 3.Verify all dependencies in requirements.txt are installed
  4. 4.Ensure .env contains valid ANTHROPICAPIKEY