hailer 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. hailer-0.1.1/.config/hailer/README.md +43 -0
  2. hailer-0.1.1/.config/hailer/context/00-example.md +30 -0
  3. hailer-0.1.1/.config/hailer/prompts/monthly-pack.md +17 -0
  4. hailer-0.1.1/.config/hailer/skills/example-skill/SKILL.md +38 -0
  5. hailer-0.1.1/.config/hailer/skills/example-skill/reference/checks.md +15 -0
  6. hailer-0.1.1/.github/workflows/members-only.yml +37 -0
  7. hailer-0.1.1/.github/workflows/release.yml +104 -0
  8. hailer-0.1.1/.github/workflows/test.yml +39 -0
  9. hailer-0.1.1/.gitignore +37 -0
  10. hailer-0.1.1/PKG-INFO +508 -0
  11. hailer-0.1.1/PLAN.md +347 -0
  12. hailer-0.1.1/README.md +490 -0
  13. hailer-0.1.1/data/.gitkeep +0 -0
  14. hailer-0.1.1/data/README.md +30 -0
  15. hailer-0.1.1/docs/INTERFACES.md +383 -0
  16. hailer-0.1.1/hailer.toml +43 -0
  17. hailer-0.1.1/notebooks/analysis.py +92 -0
  18. hailer-0.1.1/pyproject.toml +47 -0
  19. hailer-0.1.1/scripts/__init__.py +1 -0
  20. hailer-0.1.1/scripts/make_sample_data.py +103 -0
  21. hailer-0.1.1/scripts/release.py +366 -0
  22. hailer-0.1.1/src/hailer/__init__.py +3 -0
  23. hailer-0.1.1/src/hailer/agent.py +850 -0
  24. hailer-0.1.1/src/hailer/browser.py +65 -0
  25. hailer-0.1.1/src/hailer/cli.py +1680 -0
  26. hailer-0.1.1/src/hailer/config.py +550 -0
  27. hailer-0.1.1/src/hailer/context.py +259 -0
  28. hailer-0.1.1/src/hailer/errors.py +63 -0
  29. hailer-0.1.1/src/hailer/log.py +138 -0
  30. hailer-0.1.1/src/hailer/marimo_client.py +739 -0
  31. hailer-0.1.1/src/hailer/mcp_server.py +668 -0
  32. hailer-0.1.1/src/hailer/models.py +262 -0
  33. hailer-0.1.1/src/hailer/notebooks.py +543 -0
  34. hailer-0.1.1/src/hailer/periods.py +208 -0
  35. hailer-0.1.1/src/hailer/prompts/__init__.py +0 -0
  36. hailer-0.1.1/src/hailer/prompts/system.md +130 -0
  37. hailer-0.1.1/src/hailer/secrets.py +117 -0
  38. hailer-0.1.1/src/hailer/session.py +113 -0
  39. hailer-0.1.1/src/hailer/web.py +340 -0
  40. hailer-0.1.1/tests/test_agent.py +717 -0
  41. hailer-0.1.1/tests/test_browser.py +83 -0
  42. hailer-0.1.1/tests/test_cli.py +1335 -0
  43. hailer-0.1.1/tests/test_config.py +489 -0
  44. hailer-0.1.1/tests/test_context.py +242 -0
  45. hailer-0.1.1/tests/test_log.py +141 -0
  46. hailer-0.1.1/tests/test_marimo_client.py +608 -0
  47. hailer-0.1.1/tests/test_mcp_server.py +722 -0
  48. hailer-0.1.1/tests/test_notebooks.py +409 -0
  49. hailer-0.1.1/tests/test_periods.py +174 -0
  50. hailer-0.1.1/tests/test_release.py +117 -0
  51. hailer-0.1.1/tests/test_secrets.py +133 -0
  52. hailer-0.1.1/tests/test_session.py +114 -0
  53. hailer-0.1.1/tests/test_web.py +251 -0
  54. hailer-0.1.1/uv.lock +1435 -0
@@ -0,0 +1,43 @@
1
+ # Project context for Hailer
2
+
3
+ This folder holds the context, skills and prompts that Hailer gives to the AI agent for
4
+ **this** project. Commit it alongside the notebook so the whole team gets the same setup.
5
+
6
+ ```
7
+ .config/hailer/
8
+ ├── hailer.toml (optional: the project config can live here instead of the repo root)
9
+ ├── context/ always-on: every *.md here is sent to the model at the start of each session
10
+ ├── skills/ on-demand: <name>/SKILL.md (+ reference/, scripts/) loaded only when needed
11
+ └── prompts/ reusable prompts: /prompt <name> [args]
12
+ ```
13
+
14
+ ## context/
15
+
16
+ Short Markdown files with facts the agent should always know: what the datasets are, column
17
+ meanings, conventions, house style. Files are loaded in filename order (prefix with `00-`,
18
+ `10-` to control it) and concatenated up to the `max_context_bytes` limit in `hailer.toml`.
19
+
20
+ **Everything in `context/` is sent to the configured model endpoint every session.** Do not
21
+ put credentials, customer data or anything you would not paste into a chat with the model.
22
+ Hailer warns at startup if a file looks like it contains a key or token.
23
+
24
+ ## skills/
25
+
26
+ Skills use the Agent Skills format shared by Codex and Claude: a folder with a `SKILL.md`
27
+ whose frontmatter has `name` and `description`, plus optional `reference/` and `scripts/`.
28
+ Hailer puts only the name and description in the agent's instructions; the body is loaded
29
+ when the task matches (the agent calls `load_skill`) or when you type `/skill <name>`.
30
+
31
+ Write the `description` as the trigger: *when* to use the skill and *what* it produces.
32
+
33
+ ## prompts/
34
+
35
+ Plain Markdown prompts. `{{args}}` is replaced with whatever follows the name:
36
+
37
+ ```
38
+ You > /prompt monthly-pack 2025-06
39
+ ```
40
+
41
+ ## Reloading
42
+
43
+ `/reload` re-reads this folder without restarting the session. `/context` shows what is loaded.
@@ -0,0 +1,30 @@
1
+ # PRA101 analysis: team context
2
+
3
+ > This file is sent to the model endpoint at the start of every Hailer session.
4
+ > Keep it factual and short. No credentials, no customer names, no row-level data.
5
+
6
+ ## What the data is
7
+
8
+ - Monthly extracts of the PRA101 (capital+) return, one Parquet file per month in `data/`.
9
+ - Filenames encode the period as `YY-MM`, e.g. `25-03 pra101.parquet` is March 2025.
10
+ The data itself has **no** period column; Hailer adds one when loading several months.
11
+ - Later months may add columns. Treat a missing column in an older month as null, not an error.
12
+
13
+ ## Column meanings
14
+
15
+ | column | meaning |
16
+ |---|---|
17
+ | `exposure_class` | Regulatory exposure class: Corporate, Retail, Institution, Sovereign, Equity, Other |
18
+ | `counterparty_id` | Anonymised counterparty key; stable across months |
19
+ | `default_flag` | `true` when the exposure is in default. "Exclude defaults" means `default_flag == false` |
20
+ | `exposure_value` | Exposure at default in GBP |
21
+ | `rwa` | Risk-weighted assets in GBP |
22
+ | `risk_weight` | Effective risk weight (`rwa / exposure_value`); present from 2025-04 onwards |
23
+
24
+ ## Conventions
25
+
26
+ - "Latest month" means the most recent period present in `data/`, not the calendar month.
27
+ - Month-on-month movement is `latest - previous`; percentages are relative to the previous month.
28
+ - Report RWA in £m with one decimal (e.g. £1,234.5m). Keep tables to the top 10 rows unless asked.
29
+ - Prefer Polars; use DuckDB SQL when joining across many monthly files.
30
+ - Charts: bar for breakdowns by class, line for trends over periods. Label axes with units.
@@ -0,0 +1,17 @@
1
+ Produce the monthly PRA101 pack for period {{args}} (use the latest period if none is given).
2
+
3
+ In the marimo notebook, add or update these sections, reusing existing variables and cells
4
+ where they already exist:
5
+
6
+ 1. **Periods available**: a small table of the months found in `data/` and their row counts.
7
+ 2. **Headline**: total RWA and exposure value for the period and the previous month, with
8
+ movement in £m and %.
9
+ 3. **Movement by exposure class**: table sorted by absolute movement, plus a bar chart.
10
+ 4. **Top 10 movers**: counterparties with the largest absolute RWA movement between the two
11
+ months (join on `counterparty_id`), showing both months and the movement.
12
+ 5. **Data quality notes**: rows with null `exposure_class`, negative `rwa`, or
13
+ `exposure_value == 0`, counted per month.
14
+
15
+ Then reply in the terminal with a five-line summary: total movement, the largest driver, the
16
+ second-largest driver, anything unusual in the data-quality notes, and what you added to the
17
+ notebook. Do not paste tables into the terminal.
@@ -0,0 +1,38 @@
1
+ ---
2
+ name: pra101-month-on-month
3
+ description: >-
4
+ Compare the two latest PRA101 months and explain what drives the RWA movement,
5
+ producing a movement table, an exposure-class breakdown and a chart in the marimo
6
+ notebook. Use when the user asks what changed, what is driving RWA, or for a
7
+ month-on-month comparison.
8
+ ---
9
+
10
+ # PRA101 month-on-month movement
11
+
12
+ ## When to use
13
+
14
+ The user asks any of: "compare the latest two months", "what's driving the RWA increase",
15
+ "break the movement down by exposure class", "show me the biggest movers".
16
+
17
+ ## Steps
18
+
19
+ 1. `list_periods` to confirm which months exist and whether columns differ between them.
20
+ 2. In the notebook, reuse the existing `files`/`df` variables if a previous turn created them
21
+ (`notebook_cells` shows what exists). Otherwise create one cell that loads all months with
22
+ `load_periods(scan_period_files(DATA_DIR, "pra101"))` and keeps the `period` column.
23
+ 3. Create (or edit) a cell `mom` that aggregates `rwa` and `exposure_value` by `period` and
24
+ `exposure_class`, pivots the latest two periods side by side, and adds `movement` and
25
+ `movement_pct` columns. Sort by absolute movement, descending.
26
+ 4. Create a cell that shows `mom` with `mo.ui.table`, and a cell with a bar chart of
27
+ `movement` by `exposure_class` (Altair via `mo.ui.altair_chart`, or `df.plot`).
28
+ 5. If the user says "exclude defaults", filter `default_flag == False` in the loading cell and
29
+ let the dependent cells re-run; do not create a second copy of the pipeline.
30
+ 6. Run the checks in `reference/checks.md` before summarising.
31
+ 7. In the terminal, give a two-sentence summary (largest driver, total movement in £m and %),
32
+ then list what was added to the notebook.
33
+
34
+ ## Notebook hygiene
35
+
36
+ - One owning cell per public name (`files`, `df`, `mom`, `chart`); edit the owner, never redefine.
37
+ - Use `_` prefixed names for intermediates.
38
+ - Keep `hide_code=False` for analysis cells so the user can read the code.
@@ -0,0 +1,15 @@
1
+ # Reconciliation checks for a month-on-month comparison
2
+
3
+ Run these in the scratchpad (not as notebook cells) before summarising. Print only the
4
+ check name and pass/fail with the numbers involved.
5
+
6
+ 1. **Totals reconcile.** Sum of `movement` across exposure classes equals
7
+ `total_rwa(latest) - total_rwa(previous)` to within £1.
8
+ 2. **No period leakage.** The `period` column only contains the two periods being compared.
9
+ 3. **Class coverage.** Every `exposure_class` present in either month appears in the table;
10
+ a class missing from one month shows 0 for that month, not null.
11
+ 4. **Defaults handled consistently.** If defaults are excluded, both months are filtered with
12
+ the same predicate (`default_flag == False`); state the number of rows excluded per month.
13
+ 5. **Schema evolution.** If a column exists only in the latest month (e.g. `risk_weight`),
14
+ do not use it in the comparison unless the user asks; mention it once in the summary.
15
+ 6. **Sign convention.** `movement > 0` means RWA increased in the latest month.
@@ -0,0 +1,37 @@
1
+ # Close pull requests from outside the organization.
2
+ #
3
+ # GitHub has no setting that stops people opening pull requests on a public
4
+ # repository, so this closes pull requests from outsiders, leaving a short comment.
5
+ #
6
+ # A pull request is kept when its head branch lives in this repository (only people
7
+ # with write access can push one, so forks are the only route for outsiders) or when
8
+ # GitHub reports the author as OWNER, MEMBER or COLLABORATOR. The branch check comes
9
+ # first because the event payload only reports MEMBER for *public* org memberships;
10
+ # a member whose membership is private shows up as CONTRIBUTOR or NONE, which is
11
+ # how this workflow once closed a member's own pull request.
12
+ #
13
+ # pull_request_target runs on the base branch with a write token. It never checks
14
+ # out or runs code from the pull request, so that token is not exposed to it.
15
+
16
+ name: Members only
17
+
18
+ on:
19
+ pull_request_target:
20
+ types: [opened, reopened]
21
+
22
+ permissions:
23
+ pull-requests: write
24
+
25
+ jobs:
26
+ guard:
27
+ name: Close pull requests from non-members
28
+ if: ${{ github.event.pull_request.head.repo.full_name != github.repository && !contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.pull_request.author_association) }}
29
+ runs-on: ubuntu-latest
30
+ steps:
31
+ - name: Comment and close
32
+ env:
33
+ GH_TOKEN: ${{ github.token }}
34
+ PR: ${{ github.event.pull_request.number }}
35
+ run: |
36
+ gh pr comment "$PR" --repo "$GITHUB_REPOSITORY" --body "Thanks for the interest. This repository only accepts pull requests from members of the OpenAfterHours organization, so this one has been closed automatically. Please open an issue to discuss the change instead."
37
+ gh pr close "$PR" --repo "$GITHUB_REPOSITORY"
@@ -0,0 +1,104 @@
1
+ # Publish a release to PyPI.
2
+ #
3
+ # Triggered by the vX.Y.Z tag that `uv run python -m scripts.release` pushes after the
4
+ # tests pass locally. Runs the test suite again on every supported platform
5
+ # (test.yml) and builds the sdist and wheel; only if both succeed does it publish
6
+ # through the "pypi" environment using PyPI trusted publishing (OIDC, no API token
7
+ # to store) and then create the GitHub release with the distributions attached.
8
+ #
9
+ # PyPI's trusted publisher for this project is configured as:
10
+ # owner OpenAfterHours, repository hailer, workflow release.yml, environment pypi.
11
+ # Renaming this file or the environment breaks publishing.
12
+
13
+ name: Release
14
+
15
+ on:
16
+ push:
17
+ tags:
18
+ - "v*"
19
+
20
+ permissions:
21
+ contents: read
22
+
23
+ jobs:
24
+ test:
25
+ name: Tests
26
+ uses: ./.github/workflows/test.yml
27
+
28
+ build:
29
+ name: Build distributions
30
+ runs-on: ubuntu-latest
31
+ outputs:
32
+ version: ${{ steps.version.outputs.version }}
33
+ steps:
34
+ - uses: actions/checkout@v7
35
+
36
+ - uses: astral-sh/setup-uv@v10.1.0 # setup-uv publishes no floating major tag
37
+ with:
38
+ python-version: "3.12"
39
+
40
+ - name: Check that the tag matches the package version
41
+ id: version
42
+ run: |
43
+ version="$(uv version --short --color never)"
44
+ if [ "v${version}" != "${GITHUB_REF_NAME}" ]; then
45
+ echo "::error::tag ${GITHUB_REF_NAME} does not match the pyproject.toml version ${version}"
46
+ exit 1
47
+ fi
48
+ echo "version=${version}" >> "$GITHUB_OUTPUT"
49
+
50
+ - name: Build sdist and wheel
51
+ run: uv build
52
+
53
+ - name: Check distribution metadata
54
+ run: uvx twine check --strict dist/*
55
+
56
+ - uses: actions/upload-artifact@v7
57
+ with:
58
+ name: dist
59
+ path: dist/
60
+ if-no-files-found: error
61
+
62
+ publish:
63
+ name: Publish to PyPI
64
+ needs: [test, build]
65
+ runs-on: ubuntu-latest
66
+ environment:
67
+ name: pypi
68
+ url: https://pypi.org/project/hailer/${{ needs.build.outputs.version }}/
69
+ permissions:
70
+ id-token: write # mint the OIDC token PyPI trusted publishing exchanges for an upload token
71
+ steps:
72
+ - uses: actions/download-artifact@v8
73
+ with:
74
+ name: dist
75
+ path: dist/
76
+
77
+ - uses: pypa/gh-action-pypi-publish@release/v1
78
+
79
+ github-release:
80
+ name: Create GitHub release
81
+ needs: [build, publish]
82
+ runs-on: ubuntu-latest
83
+ permissions:
84
+ contents: write
85
+ steps:
86
+ - uses: actions/download-artifact@v8
87
+ with:
88
+ name: dist
89
+ path: dist/
90
+
91
+ - name: Create the release with the distributions attached
92
+ env:
93
+ GH_TOKEN: ${{ github.token }}
94
+ VERSION: ${{ needs.build.outputs.version }}
95
+ run: |
96
+ flags=()
97
+ case "$VERSION" in
98
+ *[a-z]*) flags+=(--prerelease) ;;
99
+ esac
100
+ gh release create "$GITHUB_REF_NAME" dist/* \
101
+ --repo "$GITHUB_REPOSITORY" \
102
+ --title "hailer $VERSION" \
103
+ --generate-notes \
104
+ "${flags[@]}"
@@ -0,0 +1,39 @@
1
+ # Run the test suite.
2
+ #
3
+ # Runs on pushes to main and on pull requests, and is called by release.yml so a
4
+ # release cannot be published to PyPI unless the suite passes on every platform
5
+ # here. The suite is offline (no API key, marimo server, Codex process or network).
6
+
7
+ name: Tests
8
+
9
+ on:
10
+ push:
11
+ branches: [main]
12
+ pull_request:
13
+ workflow_call:
14
+
15
+ permissions:
16
+ contents: read
17
+
18
+ jobs:
19
+ test:
20
+ name: pytest (${{ matrix.os }}, Python ${{ matrix.python }})
21
+ runs-on: ${{ matrix.os }}
22
+ strategy:
23
+ fail-fast: false
24
+ matrix:
25
+ os: [ubuntu-latest, windows-latest]
26
+ python: ["3.12", "3.13"]
27
+ steps:
28
+ - uses: actions/checkout@v7
29
+
30
+ - uses: astral-sh/setup-uv@v10.1.0 # setup-uv publishes no floating major tag
31
+ with:
32
+ python-version: ${{ matrix.python }}
33
+ enable-cache: true
34
+
35
+ - name: Install the project and the dev dependencies
36
+ run: uv sync --locked
37
+
38
+ - name: Run the tests
39
+ run: uv run --locked pytest
@@ -0,0 +1,37 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .pytest_cache/
6
+ .ruff_cache/
7
+ .mypy_cache/
8
+ build/
9
+ dist/
10
+
11
+ # uv / virtualenv
12
+ .venv/
13
+ uv.lock.bak
14
+
15
+ # Hailer runtime state (thread ids, logs) — never commit
16
+ .hailer/
17
+ *.log
18
+
19
+ # Data files stay local
20
+ data/*
21
+ !data/.gitkeep
22
+ !data/README.md
23
+
24
+ # marimo
25
+ __marimo__/
26
+ .marimo/
27
+
28
+ # Secrets — never commit
29
+ .env
30
+ *.key
31
+ *.pem
32
+
33
+ # IDE / OS
34
+ .idea/
35
+ .vscode/
36
+ .DS_Store
37
+ Thumbs.db