Skip to content

About

Construction schedule intelligence: turns messy daily site reports into a delay forecast (critical path method) and a costed recovery plan. LLM extraction with a deterministic fallback, LangGraph agent, Streamlit dashboard.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

1 Commit

Folders and files

Repository files navigation

🏗️ SitePilot AI

Construction schedule intelligence: from messy site reports to a costed recovery plan.

It turns free-text contractor daily progress reports into a structured database, computes schedule variance and a critical-path forecast, and runs an agent that finds the cheapest way to recover a slipping project.

Python Tests LangGraph Streamlit SQLite LLM


At a glance

Result
Input 30 free-text daily reports in 4 formats (typed DPR, WhatsApp, semi-tabular, rambling narrative)
Extraction accuracy (live LLM) 100% (135/135) line-to-activity match against a 135-line ground truth, 100% quantities, 100% manpower
Forecast Finish 2027-03-11 vs contract 2027-01-14: 56 days late, INR 1.04 crore liquidated-damages exposure
Root cause found A120 Pile caps & raft (own overrun 7 days) → delay chain A120 → A130 → … → A180
Recovery plan 55 days recovered for INR 22.1 lakh, 18 extra workers, within the INR 45 lakh budget and 35-worker pool
Plans evaluated 2,744 lever combinations, each one a full CPM re-run
Runs offline Yes. Every LLM step has a deterministic fallback, and the output labels which path ran

Screenshots

Overview: headline metrics, root cause, and a baseline vs forecast Gantt with the critical path in red. Overview

Recovery advisor: recommended plan, alternatives, cost vs days-saved frontier over every feasible plan, and the float check. Recovery advisor

More screenshots: Activities, Reports, About

Activities: % complete, performance factor, earned-schedule variance, float and critical flag per activity. Activities

Reports: raw report beside the parsed table, parser path used, and the data-quality exceptions panel. Reports

About: method, assumptions and known simplifications, shown in the app itself. About


The problem

Site progress arrives as free text: a typed report one day, a WhatsApp message the next, a half-filled table after that. Each one names the same work differently ("RW panels near ramp", "bsmt wall shuttering", "retaining wall at grid 4-7"). The contract programme lives somewhere else, so nobody can answer three Monday-morning questions:

  1. How late are we, really?
  2. What caused it? (as opposed to what merely inherited the delay)
  3. What is the cheapest way to recover, and what will each extra rupee buy?

When the answer is a guess, crews get thrown at activities that have float, money is spent, and the finish date doesn't move.

What SitePilot does

Stage What happens Module
1. Parse Each report is turned into a validated ParsedReport (pydantic v2) through a forced tool / function call. Aliases are mapped to baseline activities, units normalised (cum→m3, rmt→m), and bad data rejected into an exceptions log parser.py, llm.py, ingest.py
2. Reconcile Opening position plus reported quantities give % complete and a performance factor per activity. Earned-schedule variance in days, and a CPM forward/backward pass for forecast dates, total float and the critical path schedule.py, variance.py
3. Advise A LangGraph agent diagnoses the root cause, builds a lever menu (extra crew, overtime, second shift, fast-track) and searches thousands of combinations through the CPM engine under budget, labour and crowding caps. It recommends the best plan with a cheaper and a maximum-recovery alternative, then writes a site-director memo levers.py, agent.py
4. Present Streamlit dashboard with Plotly Gantt, variance table, report-vs-parse viewer and the recovery advisor app.py

Architecture

flowchart LR
    subgraph Inputs
      B[baseline_schedule.csv<br/>18 activities]
      R[30 daily reports<br/>4 free-text formats]
      O[opening_progress.csv]
    end
    R --> P{parser}
    P -- API key --> L[LLM chain<br/>Claude → Gemini → Groq<br/>forced tool call]
    P -- no key / failure --> F[deterministic<br/>regex + keyword matcher]
    L --> G[guards: date check,<br/>grounding, units,<br/>plausibility]
    F --> G
    G --> DB[(SQLite)]
    B --> DB
    O --> DB
    DB --> CPM[CPM engine<br/>forward / backward pass]
    CPM --> V[earned-schedule<br/>variance]
    V --> DB
    subgraph Agent [LangGraph recovery agent]
      LS[load_state] --> DG[diagnose]
      DG -- on time --> H[report_healthy]
      DG -- slipping --> GO[generate_options] --> SI[simulate<br/>2,744 CPM re-runs] --> EV[evaluate] --> NA[narrate] --> PE[persist]
    end
    DB --> LS
    PE --> DB
    DB --> UI[Streamlit dashboard]
Loading

Proof it works

Every claim above is backed by output committed in docs/proof/. Excerpts are shown below; the full raw logs are in that folder.

1. Test suite: 32 passing

tests/test_app.py::test_app_renders_without_exceptions PASSED
tests/test_levers.py::test_crowding_factor_monotonic_decreasing PASSED
tests/test_levers.py::test_add_crew_multiplier_formula PASSED
tests/test_levers.py::test_menu_respects_crowding_and_pool PASSED
tests/test_levers.py::test_overtime_multipliers_and_costs PASSED
tests/test_levers.py::test_plan_search_respects_budget_and_labour_caps PASSED
tests/test_levers.py::test_net_benefit_arithmetic PASSED
tests/test_levers.py::test_ld_saving_caps_at_contract_date PASSED
tests/test_levers.py::test_float_absorbs_non_critical_lever PASSED
tests/test_llm.py::test_provider_priority PASSED
tests/test_llm.py::test_gemini_structured_parse PASSED
tests/test_llm.py::test_rate_limit_is_retried PASSED
tests/test_llm.py::test_falls_through_to_groq_then_deterministic PASSED
tests/test_llm.py::test_json_in_text_is_accepted PASSED
tests/test_llm.py::test_placeholder_nulls_are_cleaned PASSED
tests/test_llm.py::test_wrong_llm_year_is_overridden_and_sentinels_dropped PASSED
tests/test_llm.py::test_hallucinated_quantity_is_not_grounded PASSED
tests/test_llm.py::test_text_call_for_narration PASSED
tests/test_llm.py::test_no_key_raises_llm_error_not_crash PASSED
tests/test_parser.py::test_fallback_line_match_accuracy PASSED
tests/test_parser.py::test_noise_lines_are_unmatched PASSED
tests/test_parser.py::test_report_dates_parse PASSED
tests/test_parser.py::test_unit_normalisation_and_mismatch_rejected PASSED
tests/test_parser.py::test_missing_key_never_raises PASSED
tests/test_schedule.py::test_forward_backward_pass_known_answers PASSED
tests/test_schedule.py::test_lever_on_floated_activity_saves_zero_days PASSED
tests/test_schedule.py::test_lever_on_critical_activity_saves_days PASSED
tests/test_schedule.py::test_in_progress_forecast_uses_observed_rate PASSED
tests/test_schedule.py::test_fast_track_negative_lag PASSED
tests/test_variance.py::test_half_done_halfway_is_on_schedule PASSED
tests/test_variance.py::test_quarter_done_halfway_is_behind PASSED
tests/test_variance.py::test_planned_pct_is_clamped PASSED
32 passed in 2.05s

Tests cover a hand-built 4-activity CPM network with known ES/EF/float answers, earned-schedule arithmetic, crowding monotonicity, budget and labour caps in the plan search, net-benefit arithmetic, parser accuracy against ground truth, unit-mismatch rejection, the LLM provider chain (against a mocked HTTP layer: retries, 429s, fallthrough), and the LLM output guards. Full log: docs/proof/pytest.txt

2. End-to-end run with no API key

make demo from a clean clone, fully offline (full log):

[ingest] LLM path: OFF - no LLM key set (ANTHROPIC_API_KEY, GEMINI_API_KEY, GOOGLE_API_KEY, GROQ_API_KEY), deterministic fallback in use
[ingest] 30 reports parsed via fallback; 135 lines, 3 unmatched, 3 exceptions; data quality 97.8%
[evaluate] activity match 135/135 = 100.0%  |  quantity 103/103 = 100.0%  |  manpower 132/132 = 100.0%  |  missed lines 0, extra lines 0
[variance] baseline finish 2027-01-14  forecast finish 2027-03-11  slip 56 days  LD exposure INR 10,360,000
[variance] root cause: A120 Pile caps and raft foundation (own overrun 7 days)
[variance] delay chain: A120 -> A130 -> A140 -> A150 -> A160 -> A170 -> A180
[advise] LLM path: OFF - no LLM key set (ANTHROPIC_API_KEY, GEMINI_API_KEY, GOOGLE_API_KEY, GROQ_API_KEY), deterministic fallback in use
[advise] RECOMMENDED: saves 55 d, direct INR 2,106,000, risk INR 105,225, total INR 2,211,225, net INR 7,963,775, peak extra labour 18, finish 2027-01-15
[advise] CHEAPER ALTERNATIVE: saves 37 d, direct INR 910,800, risk INR 105,225, total INR 1,016,025, net INR 5,828,975, peak extra labour 12, finish 2027-02-02
[advise] MAXIMUM RECOVERY: saves 66 d, direct INR 3,383,625, risk INR 0, total INR 3,383,625, net INR 6,976,375, peak extra labour 18, finish 2027-01-04
[advise] recommended within caps: budget 2,106,000 <= 4,500,000: True; labour 18 <= 35: True
           A230 second_shift  TF 163 d  days_saved = 0  cost INR 213,000  -> A230 has 163 days of total float, so accelerating it pulls its own finish in but the project finish does not move: float absorbs it. INR 213,000 for 0 days.
[advise] NARRATIVE (produced by: template (no LLM API key))
[summary] row counts: activities=18, opening_progress=6, daily_reports=30, progress_entries=135, variance_snapshots=18, recommendations=1

3. Live run on Google Gemini (free tier)

The same pipeline with GEMINI_API_KEY set (full log). Each extracted line is scored against a ground-truth file written by the data generator, so accuracy is measured, not asserted:

[ingest] LLM path: Google Gemini (free tier) (gemini-flash-lite-latest); chain: Google Gemini (free tier) -> deterministic fallback
[ingest]   DPR_2026-08-17.txt: llm 4 lines
[ingest]   DPR_2026-08-18.txt: llm 3 lines
[ingest]   DPR_2026-08-19.txt: llm 6 lines
[ingest] 30 reports parsed via llm; 144 lines, 5 unmatched, 19 exceptions; data quality 86.8%
[ingest] tokens in/out: 39148/10516
[evaluate] parser path(s): ['llm (gemini:gemini-flash-lite-latest)']
[evaluate] activity match 135/135 = 100.0%  |  quantity 103/103 = 100.0%  |  manpower 132/132 = 100.0%  |  missed lines 0, extra lines 7
[variance] baseline finish 2027-01-14  forecast finish 2027-03-11  slip 56 days  LD exposure INR 10,360,000
[variance] delay chain: A120 -> A130 -> A140 -> A150 -> A160 -> A170 -> A180
[advise] LLM path: Google Gemini (free tier) (gemini-flash-lite-latest); chain: Google Gemini (free tier) -> deterministic fallback
[agent] narrate: gemini memo, 0 warnings
[advise] RECOMMENDED: saves 55 d, direct INR 2,106,000, risk INR 105,225, total INR 2,211,225, net INR 7,963,775, peak extra labour 18, finish 2027-01-15
[advise] NARRATIVE (produced by: llm - Google Gemini (free tier) (gemini-flash-lite-latest))
  ### What slipped and why
  As of 2026-09-15, the project faces a forecast finish of 2027-03-11 against the baseline finish of 2027-01-14, representing a slip of 56 days and a li...
  ### What to do Monday morning

4. Reproducibility

Two consecutive make demo runs produce byte-identical report files and identical variance numbers, with no duplicated rows. Ingestion is idempotent on source_file. See docs/proof/determinism.txt.

5. Guarding against LLM errors: bugs found in live testing

The first live Gemini run surfaced three real failure modes. Each is now caught by a guard and covered by a regression test:

What the model did Effect if trusted Guard
Parsed WhatsApp dates like 22/08 as 2023-08-22 (8 of 30 reports) Activities appeared to have started 3 years early; float of −1,092 days LLM date is cross-checked against the deterministic header parse and overridden on disagreement
Invented "100 pct" for the remark "Surveyor setting out for ramp area" and assigned it to a finished activity Doubled that activity's quantity and skewed the portfolio performance factor Grounding check: a quantity must literally appear in its source line. Progress on already-finished activities is rejected
Returned -1, 9.99e+307, 0 "-" and "3 men" as quantities Garbage progress and false unit exceptions Placeholder and sentinel cleaning; worker counts moved to manpower; implausible quantities rejected

The same principle applies to the recovery memo: the LLM only receives the simulator's computed numbers, and any figure in the memo that is not in that data is flagged.


Method

Performance factor. PF = observed daily output / (budget_qty / baseline_duration), clamped to [0.3, 1.5]. Not-started work uses the earned-work-weighted mean PF of started work, clamped to [0.7, 1.05], and the UI labels it as assumed. Planned productivity is derived as budget_qty / (duration × crew), never hardcoded.

Earned-schedule variance (days).

planned_pct   = clamp((data_date − baseline_start + 1) / duration, 0, 1)
variance_days = pct_complete × duration − planned_pct × duration        (negative = behind)

CPM. Complete activities keep their actuals. In-progress activities finish at data_date + ⌈remaining / (baseline_rate × PF × lever)⌉. Not-started activities follow their predecessors for ⌈duration / (PF × lever)⌉ days. The backward pass gives late dates and total float, and critical means float ≤ 0. Level-of-effort activities are tracked but kept off the critical path.

Root cause. The earliest critical activity whose own duration overran by at least max(3 days, 15%). Inherited delay doesn't count, which separates the cause from its victims.

Recovery levers (C = planned crew, INR 1,150 per person-day):

Lever Output multiplier Cost
add crew k ∈ {4, 8, 12} (1 + k/C) × max(0.55, 1 − 0.30·k/C) (crowding loss) k × 1,150 / day
overtime 10 h / 12 h 1.125 / 1.20 (Business Roundtable extended-overtime curve) 0.375·C / 0.75·C × 1,150 / day
second shift 1.85 C × 1,150 × 1.25 + 18,000 / day
fast-track 30% overlap with predecessor (negative lag) expected rework 15% of activity value

Plan search. Take the cartesian product of levers over the top 4 critical activities (capped at 3,000 combinations). Each combination re-runs the full CPM and is scored with:

ld_saved    = (base slip − plan slip, floored at 0) × INR 185,000/day     # LDs stop at the contract date
net_benefit = ld_saved − (direct cost + expected rework cost)

Plans breaching the INR 45 lakh budget, the 35-worker pool (peak concurrent) or the 1.8× crowding ratio are dropped. The rest are ranked by net benefit, then by cost per day saved.

Key insight the tool surfaces: accelerating an activity that has float buys zero days. The advisor puts a second shift on the worst non-critical activities to prove it, e.g. INR 2.1 lakh on A230 moves the project finish by 0 days.

Tech stack

Python 3.11+ · pandas · SQLite (stdlib, no ORM) · pydantic v2 · LangGraph · Streamlit · Plotly · httpx · Anthropic SDK · pytest

Quickstart

git clone https://github.com/Tharun0127/SITE-PILOT-AI-.git && cd SITE-PILOT-AI-
make install      # virtualenv + dependencies
make demo         # seed -> ingest -> evaluate -> variance -> advise (works with no API key)
make app          # dashboard at http://localhost:8501
make test         # 32 tests

On Windows without GNU make, use mingw32-make demo or .venv\Scripts\python -m sitepilot.cli demo.

Optional LLM (free). Copy .env.example to .env and add a key. The first one available is used, and on failure it falls through to the next:

Priority Provider Key Default model Cost
1 Anthropic Claude ANTHROPIC_API_KEY claude-sonnet-4-5 paid
2 Google Gemini GEMINI_API_KEY (get one) gemini-flash-lite-latest free tier
3 Groq GROQ_API_KEY (get one) llama-3.3-70b-versatile free tier
4 Deterministic none regex + keyword matcher, template memo offline

Free tiers are rate-limited, so requests are paced client-side and 429s are retried with backoff. Set SITEPILOT_LLM_PROVIDER=gemini|groq|anthropic|none to pin one. Every cost-model value and the data date can be overridden by environment variable.

CLI: python -m sitepilot.cli {init | seed | ingest [--use-llm | --no-llm] | evaluate | variance | advise | demo | app | clean}

Project structure

sitepilot/
  config.py     settings, LLM provider chain, cost model, data date
  db.py         SQLite schema, idempotent upserts, loaders
  models.py     pydantic models for parsed reports
  seed.py       seeded generator: opening position, 30 reports, ground truth
  llm.py        provider chain (Claude / Gemini / Groq), retries, pacing, schema adaptation
  parser.py     LLM extraction + deterministic fallback, output cleaning, date check
  ingest.py     matching, unit normalisation, grounding and plausibility guards
  schedule.py   CPM forward/backward pass, performance factors
  variance.py   earned-schedule variance, root-cause diagnosis
  levers.py     recovery levers, plan search, float-absorption check
  agent.py      LangGraph recovery agent + memo
  evaluate.py   accuracy against ground truth
  cli.py        command line
app.py          Streamlit dashboard
tests/          32 tests
data/           baseline schedule + generated reports and ground truth
docs/           screenshots and proof logs

Data

There is no external dataset. sitepilot/seed.py generates everything from a single seeded RNG:

  • Hidden performance factors per activity: the ground truth the tool has to discover. They never appear in a report or in the database.
  • Disruption calendar: flood, RMC plant breakdown, labour dispute, rain and a festival holiday.
  • Four report formats with 4–5 aliases per activity, so exact string matching can't work.
  • Hard rules: reports never state percent complete or schedule status. The tool has to derive them.
  • ground_truth.csv records the true activity, quantity and manpower for every report line. It is used only for evaluation and never read by ingestion.

Known simplifications

  • Calendar days, not a working calendar. The 6-day week and holidays only appear as lost output.
  • Linear planned progress within each baseline window, and finish-to-start links only.
  • Synthetic data. The deterministic parser reaches 100% on it, but its abbreviation lexicon was written alongside the generator, so treat that figure as a regression test. The live LLM accuracy above is the more honest number.
  • No resource levelling. The labour pool is checked as peak concurrent extra workers.

Running on real P6 / MS Project exports

Map a P6 XER (TASK, TASKPRED, TASKRSRC) or an MS Project export onto the activities table. That needs SS/FF/SF relationships with lags and a working calendar added to the network model. Budget quantities come from the BOQ or a cost-loaded schedule. Seed each activity's keywords from the WBS path and historical reports, and replace the opening-progress CSV with the last statused update. Before trusting the forecasts, validate them against a statused P6 update.

About

Construction schedule intelligence: turns messy daily site reports into a delay forecast (critical path method) and a costed recovery plan. LLM extraction with a deterministic fallback, LangGraph agent, Streamlit dashboard.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages