Skip to content
Closed
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
76 changes: 76 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# AGENTS.md - Guidelines for Codex Agent

**Read this entire file before acting. Failing to follow these rules invalidates the task.**

## 1. Mandatory setup

1. Execute `./setup.sh` when the container starts.
- The script installs dependencies from `wheelhouse/` entirely offline.
- Stop the task if installation fails.
2. Do not add new online dependencies. The container has no internet access.

## 2. Quality gates (run in this order)

```bash
pytest -q --cov=src --cov-fail-under=90
black --check .
isort --check-only .
flake8 .
mypy src/ tests/
bandit -r src -ll
```

All commands must exit with code 0. Fix issues before committing.

## 3. Coding standards

| Topic | Mandatory rules |
| ----- | ----------------|
| Format | Use **Black** (120 char line length) and **isort** for imports. |
| Typing | Provide complete type hints. `mypy --strict` should pass without unnecessary ignores. |
| Tests | Every bug fix or feature requires unit tests. |
| Logs | Use the `logging` module, never `print`. |
| Security | Avoid `eval`, plaintext secrets and uncleaned temp files. |

## 4. Conventional commits

```
<type>(<scope>): <short summary>

<body if needed, wrapped at 72 characters>

BREAKING CHANGE: <explanation>
```

Allowed types: `feat`, `fix`, `perf`, `refactor`, `test`, `docs`, `chore`, `ci`.
Scope is the folder or module (e.g. `auth`, `api`, `db`). Keep commits atomic.

## 5. Pull request process

1. Create a branch named `feat/<issue-id>-slug` or `fix/<issue-id>-slug`.
2. Open a PR to `main` with the template:
- **What**: concise summary.
- **Why**: business need or bug description.
- **How**: technical approach (include diagrams if useful).
- **Test Plan**: commands and cases covered.
3. Ensure `make check` passes before requesting review.
4. Update `CHANGELOG.md` if needed under `## [Unreleased]`.

## 6. Standard workflow for every task

1. Analyse the issue or description.
2. Plan changes and outline them in a PR comment.
3. Implement small units (<30 lines per function).
4. Run all quality gates locally.
5. Commit following conventional commits.
6. Push the branch, open the PR, verify CI is green.

## 7. Quick troubleshooting

| Failure | Action |
| ------- | ------ |
| `ImportError` | Add the missing wheel to `wheelhouse/`, rerun `setup.sh`. |
| Coverage <90% | Add missing tests or justify drop in the PR. |
| `mypy` error | Add type hints or refactor code. |

**Remember:** *Clean, tested, documented code; no shortcuts.*
5 changes: 3 additions & 2 deletions agent/assistant.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,10 @@
"When generating PDF reports:\n"
"- IMPORTANT: When asked to create a PDF report, create it immediately with the information provided\n"
"- Generate reports even with minimal information - do not ask for clarification\n"
"- The 'data_json' parameter should be a JSON string with data to include\n"
"- Build a dictionary matching the schema in static/pdf_schema.json (title, optional insights, and a list of sections).\n"
" Each section has a type of 'paragraph', 'table', or 'chart'. Charts require 'chart_type' (bar, pie or line) with labels and values.\n"
"- Pass this dictionary as a JSON string via the 'data_json' parameter\n"
"- Always include the generated PDF file path in your response\n"
'- Example format: {"title": "Report Title", "data": "Your Data"}\n'
'- To call the PDF tool, use: {"name": "pdf", "arguments": {"data_json": "JSON string here"}}\n\n'
"When working with CSV files:\n"
"- If a user has uploaded a CSV file, it will be available in the uploads directory\n"
Expand Down
18 changes: 13 additions & 5 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,19 @@
import os
import uuid
import shutil
from pathlib import Path
from jsonschema import validate, ValidationError
from tools.sql_tool import run_sql
from tools.csv_tool import summarise_csv
from tools.pdf_tool import create_pdf
from tools.default_paths import DATA_DIR, UPLOADS_DIR

# assistant
from agent import answer, _check_ollama_available, session_manager

# Load PDF schema for validation
PDF_SCHEMA_PATH = Path("static/pdf_schema.json")
with open(PDF_SCHEMA_PATH, "r", encoding="utf-8") as _f:
PDF_SCHEMA = json.load(_f)


def server_status() -> str:
"""
Expand Down Expand Up @@ -163,13 +168,16 @@ def create_pdf_wrapper(data_json, out_path=None, include_chart=True):
),
}
else:
# Use the data directly
data = data_json

try:
# Handle basic data type conversion
if isinstance(data, dict):
# Dictionary - use as is
if "sections" in data:
try:
validate(instance=data, schema=PDF_SCHEMA)
except ValidationError as ve:
data = {"error": "Invalid PDF schema", "details": ve.message}
pass
elif isinstance(data, list):
# Convert list to simple dictionary with indexed keys
Expand Down Expand Up @@ -337,7 +345,7 @@ def respond(message, history, model_choice, csv_file, session_id=None, prev_resu

# Log if we have a previous result object
if prev_result:
print(f"Using previous result object for conversation continuity")
print("Using previous result object for conversation continuity")
else:
print("No previous result object available")

Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,4 @@ tzdata==2025.2
urllib3==2.4.0
uvicorn==0.34.2
websockets==12.0
jsonschema==4.22.0
55 changes: 55 additions & 0 deletions static/pdf_schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "PDF Report Schema",
"type": "object",
"description": "Structure for creating rich PDF reports with cover page, insights and various section types.",
"properties": {
"title": {
"type": "string",
"description": "Main report title"
},
"cover": {
"type": "object",
"description": "Optional cover page settings",
"properties": {
"logo_path": {
"type": "string",
"description": "Path to a logo image"
}
},
"additionalProperties": false
},
"insights": {
"type": "array",
"description": "List of insight paragraphs to highlight on the first page",
"items": {"type": "string"}
},
"sections": {
"type": "array",
"description": "Report sections such as paragraphs, tables or charts",
"items": {
"type": "object",
"properties": {
"title": {"type": "string"},
"type": {"type": "string", "enum": ["paragraph", "table", "chart"]},
"text": {"type": "string"},
"data": {"type": ["array", "object"]},
"chart_spec": {
"type": "object",
"properties": {
"chart_type": {"type": "string", "enum": ["bar", "pie", "line"]},
"labels": {"type": "array", "items": {"type": "string"}},
"values": {"type": "array", "items": {"type": "number"}}
},
"required": ["chart_type", "labels", "values"],
"additionalProperties": false
}
},
"required": ["title", "type"],
"additionalProperties": false
}
}
},
"required": ["title", "sections"],
"additionalProperties": false
}
51 changes: 51 additions & 0 deletions tests/test_pdf_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,3 +231,54 @@ def test_empty_value_handling(tmp_path):
}
output_path = create_pdf(data, out_path=tmp_path / "empty_values.pdf")
assert Path(output_path).exists()


def _count_images(pdf_path: Path) -> int:
with open(pdf_path, "rb") as f:
return f.read().count(b"/Subtype /Image")


def test_pdf_with_sections_and_charts(tmp_path):
"""Generate a PDF using the new schema with multiple chart types."""
data = {
"title": "Complex Report",
"insights": ["Insight one", "Another insight"],
"sections": [
{
"title": "Numbers",
"type": "table",
"data": [{"a": 1, "b": 2}, {"a": 3, "b": 4}],
},
{
"title": "Bar Chart",
"type": "chart",
"chart_spec": {
"chart_type": "bar",
"labels": ["A", "B"],
"values": [1, 2],
},
},
{
"title": "Pie Chart",
"type": "chart",
"chart_spec": {
"chart_type": "pie",
"labels": ["X", "Y"],
"values": [3, 7],
},
},
{
"title": "Line Chart",
"type": "chart",
"chart_spec": {
"chart_type": "line",
"labels": [1, 2, 3],
"values": [1, 4, 9],
},
},
],
}

pdf_path = Path(create_pdf(data, out_path=tmp_path / "complex.pdf"))
assert pdf_path.exists()
assert _count_images(pdf_path) >= 4
Loading