Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,27 @@ new logic.

## Test status
`pytest tests/ -v` → **61 passed** (40 pre-existing + 21 new), no failures.

---

## Addendum — report generation (closes the agentic-workflow trio)

The JD asks for "agentic workflows for reasoning, **verification, and report
generation**." Reasoning (`agent.py`) and verification (`verification.py`)
were already present; report generation was the remaining gap. Added:

| File | What it does |
|------|--------------|
| `report.py` | `generate_report(events, mode, title)` → `CapitalReport`. Computes the **facts in Python** (chronological timeline, per-type rollups, headline capital metrics, embedded verification) so figures are deterministic and never hallucinated, attaches an **inline `(p. N)` page citation** to every claim (pages never invented — same rule as `schema.citation_from_hit`), and renders a Markdown brief. `mode="extractive"` is free/offline; `mode="llm"` adds a Claude-written executive summary **over the computed skeleton only**, and falls back to extractive if the call fails. |
| `tests/test_report.py` | Rollup metrics, chronological ordering, page-only citation rule, embedded verification (clean + planted continuity break), Markdown rendering with citations, custom title, empty-input safety. |

### Modified
| File | Change |
|------|--------|
| `api.py` | Added `POST /report` `{mode, title}` → structured fields + rendered Markdown. No change to existing endpoints. |
| `tests/test_api.py` | Added `/report` happy-path, bad-mode (400), and custom-title tests. |
| `requirements-api.txt` | Added `anthropic` so `/ask?mode=llm` and `/report?mode=llm` work from the documented quickstart (server still boots and serves extractive routes without a key). |
| `README.md` | Refreshed to the live numbers (F1 0.968, 73 tests), full endpoint table, and the new report stage. |

### Test status
`pytest tests/ -q` → **73 passed** (61 prior + 12 new), no failures.
53 changes: 40 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

[![Tests](https://github.com/Utkal059/capscribe/actions/workflows/tests.yml/badge.svg)](https://github.com/Utkal059/capscribe/actions)

CapScribe parses dense regulatory PDF documents (DRHPs, IPO prospectuses) and extracts structured capital event data — allotments, bonus issues, rights issues, and authorised capital changes — into clean, machine-readable JSON. Built for analysts, quant researchers, and fintech pipelines that need reliable signal from unstructured filings.
CapScribe parses dense regulatory PDF documents (DRHPs, IPO prospectuses, annual reports) and extracts structured capital event data — allotments, bonus issues, rights issues, authorised-capital changes, dividends, buybacks, and warrant exercises — into clean, machine-readable JSON. Every event is traceable back to the exact page and verbatim text it came from. Built for analysts, quant researchers, and fintech pipelines that need reliable signal from unstructured filings.

## Demo

Expand All @@ -26,44 +26,71 @@ Structured investor intelligence synthesised from raw DRHP events via Claude.
| Bonus Issues | Date, ratio, pre/post share count |
| Rights Issues | Date, ratio, price, record date |
| Authorised Capital Changes | Date, from/to amount, resolution type |
| Dividends | Date, amount per share, record/payment date, total outflow |
| Share Repurchases | Date, shares bought back, remaining authority |
| Warrant Exercises | Date, warrants exercised, exercise price |

## Architecture

- **Extraction** — PDF parser producing structured JSON events per `schema.py`
- **Retrieval** — ChromaDB vector store with all-MiniLM-L6-v2 embeddings
- **Agent** — LangGraph ReAct loop over `search_events` / `get_event_detail` tools
- **API** — FastAPI service (`/health`, `/stats`, `/search`, `/ask`)
- **Frontend** — Financial terminal UI (dark theme, semantic search, extractive + LLM QA)
- **Tables** — direct `pdfplumber` table extraction (`table_extractor.py`), merged-cell aware, preferred over LLM events on a fuzzy match
- **OCR** — scanned-page fallback (`ocr.py`) via tesseract, degrades gracefully when the binary is absent
- **Retrieval** — hybrid BM25 + ChromaDB vectors fused with reciprocal rank fusion (`retrieval.py`); an auto-alpha heuristic leans toward BM25 for numeric queries
- **Agent** — observable LangGraph state machine `retrieve → grade → synthesize → validate` (`agent.py`)
- **Verification** — deterministic contradiction checks: timeline / capital-continuity / bonus-arithmetic (`verification.py`)
- **Report** — source-backed capital-history brief with inline page citations (`report.py`)
- **API** — FastAPI service (`api.py`)
- **Frontend** — financial terminal UI (dark theme, semantic search, extractive + LLM QA, citation pills)

## API

| Endpoint | Purpose |
|---|---|
| `GET /health` | liveness; reports retrieval mode and `ocr_available` |
| `GET /stats` | event counts by type |
| `GET /events` | list events (optional `?event_type=` & `?limit=`) |
| `POST /search` | hybrid search `{query, k, alpha}` |
| `POST /ask` | agentic RAG `{question, mode}` (extractive / llm) |
| `POST /verify` | full-corpus contradiction report |
| `POST /report` | source-backed capital-history brief `{mode, title}` |
| `POST /ingest` | PDF upload → OCR fallback → table extraction |
| `POST /index` | rebuild the index from a different extracted JSON |

## Evaluation (sample DRHP)

| Metric | Score |
|---|---|
| Precision | 1.000 |
| Recall | 0.857 |
| F1 | 0.923 |
| Recall | 0.938 |
| F1 | 0.968 |

Per-event-type and per-extraction-method breakdowns are emitted by `evaluate.py`; retrieval quality (nDCG@5, MRR — dense vs hybrid) by `benchmark_retrieval.py`.

## Quickstart

```bash
cp .env.example .env # add ANTHROPIC_API_KEY
pip install -r requirements-api.txt
cp .env.example .env # add ANTHROPIC_API_KEY (only needed for llm modes)
pip install -r requirements.txt -r requirements-api.txt
uvicorn api:app --reload
# open http://localhost:8000
```

The index builds on startup from `fixtures/sample_events.json` using local embeddings, so search / ask / verify / report all run with **zero API spend**. Only `mode="llm"` calls Claude.

## Tests

```bash
pytest -q # 15 passed
pip install -r requirements.txt -r requirements-api.txt -r requirements-dev.txt
pytest -q # 73 passed
```

## Eval Harness
## Eval & Benchmarks

```bash
python evaluate.py fixtures/sample_events.json fixtures/gold_events.json
```
python benchmark_retrieval.py # nDCG@5 + MRR, dense vs hybrid
```

## License

MIT
MIT
23 changes: 23 additions & 0 deletions api.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
GET /events list events (optional ?event_type= & ?limit=)
POST /search semantic search {query, k}
POST /ask agentic RAG {question, mode}
POST /verify full-corpus contradiction report
POST /report source-backed capital-history brief {mode, title}
POST /ingest PDF upload -> OCR fallback + table extraction
POST /index rebuild the index from a different extracted JSON

The index is built once on startup from settings.events_path using local
Expand All @@ -31,6 +34,7 @@
from agent import CapScribeAgent
from config import settings
from ocr import ocr_available, process_document
from report import generate_report
from retrieval import EventStore, HybridRetriever, load_events
from table_extractor import TableExtractor, merge_with_dedup
from verification import events_from_store, verify_report
Expand Down Expand Up @@ -77,6 +81,11 @@ class IndexRequest(BaseModel):
events_path: str


class ReportRequest(BaseModel):
mode: str = "extractive" # "extractive" (free) | "llm" (Claude summary)
title: str | None = None


def _build(events_path: str) -> int:
store = EventStore() # default local embeddings, persistent
n = store.index_events(load_events(events_path))
Expand Down Expand Up @@ -161,6 +170,20 @@ def verify() -> dict:
return verify_report(events).model_dump()


@app.post("/report")
def report(req: ReportRequest) -> dict:
"""Generate a source-backed capital-history brief over the indexed filing.

``mode="extractive"`` (default) is free and deterministic; ``mode="llm"``
adds a Claude-written executive summary over the same computed facts.
Returns structured fields plus a rendered Markdown brief.
"""
if req.mode not in ("extractive", "llm"):
raise HTTPException(400, "mode must be 'extractive' or 'llm'")
events = events_from_store(_retriever())
return generate_report(events, mode=req.mode, title=req.title).model_dump()


@app.post("/ingest")
async def ingest(file: UploadFile = File(...)) -> dict:
"""Ingest a PDF: OCR-fallback text extraction → table extraction → summary.
Expand Down
Loading
Loading