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
16 changes: 16 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
.git
.gitignore
.DS_Store
.env
.env.*
!.env.example
__pycache__/
.pytest_cache/
.venv/
.vscode/
node_modules/
ui/node_modules/
ui/dist/
dist/
build/
coverage/
4 changes: 1 addition & 3 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,10 @@ APP_NAME=GTM Analytics Copilot
APP_ENV=development
API_HOST=0.0.0.0
API_PORT=8000
STREAMLIT_PORT=8501
REFERENCE_DATE=2026-03-21
LLM_PROVIDER=openai
GEMINI_API_KEY=''cd
GEMINI_API_KEY=''
GEMINI_MODEL=gemini-2.5-flash
OPENAI_API_KEY=''
OPENAI_MODEL=gpt-4.1-mini
LOG_LEVEL=INFO
API_BASE_URL=http://localhost:8000
43 changes: 43 additions & 0 deletions .github/workflows/frontend-checks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Frontend Checks

on:
pull_request:
paths:
- "ui/**"
- ".github/workflows/frontend-checks.yml"
push:
branches:
- main
paths:
- "ui/**"
- ".github/workflows/frontend-checks.yml"

jobs:
frontend:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ui

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
cache-dependency-path: ui/package-lock.json

- name: Install dependencies
run: npm install

- name: Lint
run: npm run lint

- name: Test
run: npm run test:run

- name: Build
run: npm run build
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 8000 8501
EXPOSE 8000

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
28 changes: 17 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,12 @@ Core modules:
- `app/agent/analysis.py`: single-pass narrative from query + steps
- `app/agent/graph.py`: LangGraph orchestration
- `app/api/routes.py`: API surface
- `ui/streamlit_app.py`: demo UI
- `ui/`: React + Vite frontend

## Repo Structure

```text
gtm-copilot/
planera/
├── app/
├── ui/
├── data/
Expand All @@ -93,7 +93,7 @@ gtm-copilot/
### 1. Create the environment

```bash
cd gtm-copilot
cd planera
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
Expand All @@ -110,6 +110,8 @@ API endpoints:

- `GET /health`
- `GET /sample-questions`
- `POST /uploads`
- `GET /inspections/{inspection_id}`
- `POST /analyze`

Example request:
Expand All @@ -120,29 +122,33 @@ curl -X POST http://localhost:8000/analyze \
-d '{"query":"Why did pipeline velocity drop this week?"}'
```

### 3. Run the Streamlit UI
### 3. Run the React UI

In a second terminal:

```bash
streamlit run ui/streamlit_app.py
cd ui
npm install
npm run dev
```

## Environment Variables

Defined in `.env.example`:
Backend settings are defined in `.env.example`:

- `APP_NAME`
- `APP_ENV`
- `API_HOST`
- `API_PORT`
- `STREAMLIT_PORT`
- `GEMINI_API_KEY`
- `GEMINI_MODEL`
- `OPENAI_API_KEY`
- `OPENAI_MODEL`
- `LOG_LEVEL`
- `API_BASE_URL`

Set `LLM_PROVIDER` to `openai` or `gemini` and provide the matching API key (`OPENAI_API_KEY` or `GEMINI_API_KEY`). This version does not include a non-LLM fallback path.
Frontend settings live in `ui/.env.example`.

Set `LLM_PROVIDER` to `openai` or `gemini` and provide the matching API key (`OPENAI_API_KEY` or `GEMINI_API_KEY`).

## Data Model

Expand Down Expand Up @@ -195,11 +201,11 @@ docker compose up --build
Then open:

- API: [http://localhost:8000](http://localhost:8000)
- UI: [http://localhost:8501](http://localhost:8501)
- UI: [http://localhost:5173](http://localhost:5173)

## Demo Script

1. Open the Streamlit UI.
1. Open the Planera UI.
2. Select "Why did pipeline velocity drop this week?"
3. Run the analysis and show the planner-executor loop spinner.
4. Open the executed-steps panel and show the generated SQL or pandas code.
Expand Down
81 changes: 44 additions & 37 deletions app/agent/analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,53 +4,60 @@

import json

from app.agent.analysis_grounding import build_analysis_evidence, build_approved_claims, validate_rendered_analysis
from app.agent.state import AnalysisState
from app.llm import get_llm_client
from app.prompts import render_prompt
from app.schemas import AnalysisRenderResponse

_ANALYSIS_RENDER_ATTEMPTS = 2


def _build_analysis_render_prompt(
question: str,
approved_claims_json: str,
validation_feedback: str | None = None,
) -> str:
return render_prompt(
"analysis_render.j2",
question=question,
approved_claims_json=approved_claims_json,
validation_feedback=validation_feedback,
)


def run_analysis_narrative(state: AnalysisState) -> AnalysisState:
"""Produce markdown-friendly analysis from query, objective, and step outputs."""

plan = state.get("compiled_plan") or {}
objective = plan.get("objective") or ""
metric = plan.get("metric") or state.get("metric") or ""
metric_direction = plan.get("metric_direction") or ""

workflow = state.get("workflow_status", "")
failure_note = ""
if workflow in ("planner_failed", "execution_failed"):
errs = state.get("errors") or []
summary = "; ".join(e.get("message", "") for e in errs[-3:]) if errs else "See trace and errors."
failure_note = f"\nWorkflow note: execution did not complete successfully ({workflow}). {summary}\n"

prompt = f"""
You are a GTM analytics analyst. Explain what the data shows in response to the user's question.

Rules:
- Base conclusions only on the executed steps and their artifacts below. Do not invent numbers.
- If there were no successful steps or data is insufficient, say so clearly.
- Use markdown: short headings, bullets where helpful.
- Return JSON only in this shape:
{{ "analysis": "<markdown string>" }}

User question:
{state["query"]}

Analytical objective (from planner):
{objective}

Primary metric (if provided): {metric}
Metric directionality (if provided): {metric_direction}
{failure_note}
Dataset schema (reference):
{json.dumps(state["dataset_context"], indent=2)}

Executed steps (with previews where available):
{json.dumps(state["executed_steps"], indent=2)}
"""
state["analysis"] = "The available evidence is insufficient because the workflow did not complete successfully."
return state

evidence = build_analysis_evidence(state)
approved_claims, expected_status = build_approved_claims(evidence)
if not approved_claims:
state["analysis"] = "The approved claims are insufficient to answer the question with the available evidence."
return state

approved_claims_json = json.dumps([claim.model_dump() for claim in approved_claims], indent=2)
feedback: str | None = None

try:
result = get_llm_client().generate_json(prompt)
state["analysis"] = result.get("analysis", "").strip() or "No analysis text was returned."
for attempt in range(1, _ANALYSIS_RENDER_ATTEMPTS + 1):
prompt = _build_analysis_render_prompt(state["query"], approved_claims_json, validation_feedback=feedback)
result = get_llm_client().generate_json(prompt, schema=AnalysisRenderResponse)
parsed = result if isinstance(result, AnalysisRenderResponse) else AnalysisRenderResponse.model_validate(result)
try:
validate_rendered_analysis(parsed, approved_claims, expected_status)
except ValueError as exc:
feedback = str(exc)
if attempt >= _ANALYSIS_RENDER_ATTEMPTS:
raise
continue

state["analysis"] = parsed.analysis_markdown.strip() or "No analysis text was returned."
return state
except Exception as exc: # pragma: no cover - defensive
state["analysis"] = (
f"The analysis step could not complete ({exc!s}). "
Expand Down
Loading
Loading