labagent 0.1.0__tar.gz → 0.2.0__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.

Potentially problematic release.


This version of labagent might be problematic. Click here for more details.

Files changed (98) hide show
  1. labagent-0.2.0/PKG-INFO +120 -0
  2. labagent-0.2.0/README.md +82 -0
  3. labagent-0.2.0/docs/packs.md +111 -0
  4. labagent-0.2.0/labagent/__init__.py +3 -0
  5. labagent-0.2.0/labagent/cli.py +154 -0
  6. labagent-0.2.0/labagent/datasets.py +101 -0
  7. labagent-0.2.0/labagent/nl.py +246 -0
  8. labagent-0.2.0/labagent/packs/__init__.py +5 -0
  9. labagent-0.2.0/labagent/packs/base.py +144 -0
  10. labagent-0.2.0/labagent/packs/databricks/__init__.py +2 -0
  11. labagent-0.2.0/labagent/packs/databricks/auth.py +38 -0
  12. labagent-0.2.0/labagent/packs/databricks/cli.py +73 -0
  13. labagent-0.2.0/labagent/packs/databricks/pg.py +34 -0
  14. labagent-0.2.0/labagent/packs/db_agent/pack.py +269 -0
  15. labagent-0.2.0/labagent/packs/gitsrc.py +45 -0
  16. labagent-0.2.0/labagent/packs/rag/__init__.py +0 -0
  17. labagent-0.2.0/labagent/packs/rag/app/app.py +106 -0
  18. labagent-0.2.0/labagent/packs/rag/app/app.yaml +16 -0
  19. labagent-0.2.0/labagent/packs/rag/app/requirements.txt +4 -0
  20. labagent-0.2.0/labagent/packs/rag/app/retrieval.py +45 -0
  21. labagent-0.2.0/labagent/packs/rag/app/static/index.html +137 -0
  22. labagent-0.2.0/labagent/packs/rag/chunking.py +49 -0
  23. labagent-0.2.0/labagent/packs/rag/pack.py +329 -0
  24. labagent-0.2.0/labagent/packs/rag/sources/__init__.py +53 -0
  25. labagent-0.2.0/labagent/packs/rag/sources/builtin.py +41 -0
  26. labagent-0.2.0/labagent/packs/rag/sources/builtin_corpus.jsonl +25 -0
  27. labagent-0.2.0/labagent/packs/rag/sources/sec.py +237 -0
  28. labagent-0.2.0/labagent/packs/registry.py +49 -0
  29. labagent-0.2.0/labagent.egg-info/PKG-INFO +120 -0
  30. {labagent-0.1.0 → labagent-0.2.0}/labagent.egg-info/SOURCES.txt +31 -0
  31. {labagent-0.1.0 → labagent-0.2.0}/labagent.egg-info/requires.txt +3 -0
  32. {labagent-0.1.0 → labagent-0.2.0}/pyproject.toml +6 -2
  33. labagent-0.2.0/tests/__init__.py +0 -0
  34. labagent-0.2.0/tests/test_datasets.py +73 -0
  35. labagent-0.2.0/tests/test_db_agent.py +259 -0
  36. labagent-0.2.0/tests/test_nl.py +207 -0
  37. labagent-0.2.0/tests/test_packs.py +369 -0
  38. labagent-0.2.0/tests/test_rag_app.py +109 -0
  39. labagent-0.2.0/tests/test_rag_sec.py +169 -0
  40. labagent-0.1.0/PKG-INFO +0 -73
  41. labagent-0.1.0/README.md +0 -37
  42. labagent-0.1.0/labagent/__init__.py +0 -3
  43. labagent-0.1.0/labagent/cli.py +0 -63
  44. labagent-0.1.0/labagent.egg-info/PKG-INFO +0 -73
  45. {labagent-0.1.0 → labagent-0.2.0}/CONTRIBUTING.md +0 -0
  46. {labagent-0.1.0 → labagent-0.2.0}/LICENSE +0 -0
  47. {labagent-0.1.0 → labagent-0.2.0}/MANIFEST.in +0 -0
  48. {labagent-0.1.0 → labagent-0.2.0}/SECURITY.md +0 -0
  49. {labagent-0.1.0 → labagent-0.2.0}/docs/account-cleanup.md +0 -0
  50. {labagent-0.1.0 → labagent-0.2.0}/docs/e2e-test-plan.md +0 -0
  51. {labagent-0.1.0 → labagent-0.2.0}/docs/examples/oidc-trust-policy.example.json +0 -0
  52. {labagent-0.1.0 → labagent-0.2.0}/docs/examples/permissions-read.example.json +0 -0
  53. {labagent-0.1.0 → labagent-0.2.0}/docs/examples/permissions-write.example.json +0 -0
  54. {labagent-0.1.0 → labagent-0.2.0}/docs/examples/runner-workflow.example.yml +0 -0
  55. {labagent-0.1.0 → labagent-0.2.0}/docs/job-contract.md +0 -0
  56. {labagent-0.1.0 → labagent-0.2.0}/docs/state-and-secrets.md +0 -0
  57. {labagent-0.1.0 → labagent-0.2.0}/examples/config.example.json +0 -0
  58. {labagent-0.1.0 → labagent-0.2.0}/examples/rulesets/legacy-cleanup.example.json +0 -0
  59. {labagent-0.1.0 → labagent-0.2.0}/labagent/audit.py +0 -0
  60. {labagent-0.1.0 → labagent-0.2.0}/labagent/cleanup.py +0 -0
  61. {labagent-0.1.0 → labagent-0.2.0}/labagent/config.py +0 -0
  62. {labagent-0.1.0 → labagent-0.2.0}/labagent/context.py +0 -0
  63. {labagent-0.1.0 → labagent-0.2.0}/labagent/deleters.py +0 -0
  64. {labagent-0.1.0 → labagent-0.2.0}/labagent/discovery.py +0 -0
  65. {labagent-0.1.0 → labagent-0.2.0}/labagent/envelope.py +0 -0
  66. {labagent-0.1.0 → labagent-0.2.0}/labagent/iam_ops.py +0 -0
  67. {labagent-0.1.0/tests → labagent-0.2.0/labagent/packs/db_agent}/__init__.py +0 -0
  68. {labagent-0.1.0 → labagent-0.2.0}/labagent/plan.py +0 -0
  69. {labagent-0.1.0 → labagent-0.2.0}/labagent/rules.py +0 -0
  70. {labagent-0.1.0 → labagent-0.2.0}/labagent/run.py +0 -0
  71. {labagent-0.1.0 → labagent-0.2.0}/labagent/secrets_box.py +0 -0
  72. {labagent-0.1.0 → labagent-0.2.0}/labagent/sweep.py +0 -0
  73. {labagent-0.1.0 → labagent-0.2.0}/labagent/terraform/cohort_iam/.terraform.lock.hcl +0 -0
  74. {labagent-0.1.0 → labagent-0.2.0}/labagent/terraform/cohort_iam/main.tf +0 -0
  75. {labagent-0.1.0 → labagent-0.2.0}/labagent/terraform/cohort_iam/outputs.tf +0 -0
  76. {labagent-0.1.0 → labagent-0.2.0}/labagent/terraform/cohort_iam/sandbox-policy.json.tftpl +0 -0
  77. {labagent-0.1.0 → labagent-0.2.0}/labagent/terraform/cohort_iam/tests/cohort.tftest.hcl +0 -0
  78. {labagent-0.1.0 → labagent-0.2.0}/labagent/terraform/cohort_iam/variables.tf +0 -0
  79. {labagent-0.1.0 → labagent-0.2.0}/labagent/terraform/cohort_iam/versions.tf +0 -0
  80. {labagent-0.1.0 → labagent-0.2.0}/labagent/terraform/state_backend/.terraform.lock.hcl +0 -0
  81. {labagent-0.1.0 → labagent-0.2.0}/labagent/terraform/state_backend/main.tf +0 -0
  82. {labagent-0.1.0 → labagent-0.2.0}/labagent/terraform/state_backend/tests/bootstrap.tftest.hcl +0 -0
  83. {labagent-0.1.0 → labagent-0.2.0}/labagent/tf.py +0 -0
  84. {labagent-0.1.0 → labagent-0.2.0}/labagent.egg-info/dependency_links.txt +0 -0
  85. {labagent-0.1.0 → labagent-0.2.0}/labagent.egg-info/entry_points.txt +0 -0
  86. {labagent-0.1.0 → labagent-0.2.0}/labagent.egg-info/top_level.txt +0 -0
  87. {labagent-0.1.0 → labagent-0.2.0}/scripts/check_no_pii.py +0 -0
  88. {labagent-0.1.0 → labagent-0.2.0}/scripts/make_test_job.py +0 -0
  89. {labagent-0.1.0 → labagent-0.2.0}/setup.cfg +0 -0
  90. {labagent-0.1.0 → labagent-0.2.0}/tests/conftest.py +0 -0
  91. {labagent-0.1.0 → labagent-0.2.0}/tests/test_classify.py +0 -0
  92. {labagent-0.1.0 → labagent-0.2.0}/tests/test_cleanup.py +0 -0
  93. {labagent-0.1.0 → labagent-0.2.0}/tests/test_cloudfront_and_cli.py +0 -0
  94. {labagent-0.1.0 → labagent-0.2.0}/tests/test_iam_ops.py +0 -0
  95. {labagent-0.1.0 → labagent-0.2.0}/tests/test_plan.py +0 -0
  96. {labagent-0.1.0 → labagent-0.2.0}/tests/test_repo_hygiene.py +0 -0
  97. {labagent-0.1.0 → labagent-0.2.0}/tests/test_sweep_moto.py +0 -0
  98. {labagent-0.1.0 → labagent-0.2.0}/tests/test_tf_wrapper.py +0 -0
@@ -0,0 +1,120 @@
1
+ Metadata-Version: 2.4
2
+ Name: labagent
3
+ Version: 0.2.0
4
+ Summary: Open-source CLI and AI agent to launch and tear down hands-on labs for RAG, LLM and Databricks, plus AWS and Terraform sandboxes.
5
+ Author: BeCloudReady
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/becloudready/labagent
8
+ Project-URL: Source, https://github.com/becloudready/labagent
9
+ Project-URL: Issues, https://github.com/becloudready/labagent/issues
10
+ Project-URL: Documentation, https://github.com/becloudready/labagent/tree/main/docs
11
+ Keywords: hands-on-labs,rag,llm,databricks,lakebase,pgvector,text-to-sql,ollama,ai-agent,aws,terraform,training,workshop,sandbox,cleanup,iam,boto3
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: Education
16
+ Classifier: Intended Audience :: System Administrators
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Topic :: Software Development :: Build Tools
24
+ Classifier: Topic :: System :: Systems Administration
25
+ Requires-Python: >=3.9
26
+ Description-Content-Type: text/markdown
27
+ License-File: LICENSE
28
+ Requires-Dist: boto3>=1.34
29
+ Requires-Dist: cryptography>=42
30
+ Provides-Extra: rag
31
+ Requires-Dist: psycopg[binary]>=3.1; extra == "rag"
32
+ Provides-Extra: dev
33
+ Requires-Dist: pytest>=8; extra == "dev"
34
+ Requires-Dist: moto[all]>=5; extra == "dev"
35
+ Requires-Dist: build>=1.2; extra == "dev"
36
+ Requires-Dist: twine>=5; extra == "dev"
37
+ Dynamic: license-file
38
+
39
+ # labagent
40
+
41
+ labagent is an open-source CLI that launches hands-on labs for RAG, LLM apps and Databricks, then removes them so you do not pay for leftovers. It also sets up and cleans AWS training sandboxes with Terraform.
42
+
43
+ ## Quick start
44
+
45
+ ```bash
46
+ pip install 'labagent[rag]' # Python 3.9+; labs use the Databricks CLI: databricks auth login
47
+ labagent packs # list the labs
48
+ labagent launch rag-sec-filings --name demo
49
+ labagent destroy rag-sec-filings --name demo
50
+ ```
51
+
52
+ Every step is safe to re-run. `launch` resumes where it stopped. `destroy` removes only what the lab created.
53
+
54
+ ## Lab catalog
55
+
56
+ | Lab | What you learn | Cost | Command |
57
+ |---|---|---|---|
58
+ | `rag-sec-filings` | RAG over real SEC 10-K filings: chunking, hybrid search, citations, a quality gate before deploy | Databricks Free Edition | `labagent launch rag-sec-filings --name demo` |
59
+ | `rag-lakebase` | The same RAG pipeline on a small demo corpus, to smoke-test your setup | Databricks Free Edition | `labagent launch rag-lakebase --name demo` |
60
+ | `rag-sec-filings-live` | Fetch fresh filings from SEC EDGAR at launch | Databricks Free Edition | `labagent launch rag-sec-filings-live --name demo --param "user_agent=Your Name you@example.com"` |
61
+ | `db-agent` | Deploy [db-agent](https://github.com/db-agent/db-agent), a text-to-SQL agent, over a Databricks sample dataset | Databricks Free Edition | `labagent launch db-agent --name demo` |
62
+
63
+ Free Edition limits apply: one Lakebase project, three apps, and apps stop after 24 hours. Details: [docs/packs.md](docs/packs.md).
64
+
65
+ ## Run a RAG lab on Databricks Lakebase
66
+
67
+ `rag-sec-filings` builds a RAG app on Postgres with pgvector. It loads a pinned snapshot of 10-K filings, runs a retrieval quality check, and deploys the app only if the check passes. The app has company and year filters, suggested questions and cited answers.
68
+
69
+ ```bash
70
+ labagent launch rag-sec-filings --name demo
71
+ ```
72
+
73
+ The data comes from a checksummed release in [labagent-datasets](https://github.com/becloudready/labagent-datasets). Known gaps are listed in its README.
74
+
75
+ ## Text-to-SQL agent on Databricks
76
+
77
+ `db-agent` clones [github.com/db-agent/db-agent](https://github.com/db-agent/db-agent) at a pinned commit and deploys it to Databricks Apps. It then asks a test question through the live app. The data is the `samples` catalog every workspace has.
78
+
79
+ ```bash
80
+ labagent launch db-agent --name demo --param dataset=bakehouse
81
+ ```
82
+
83
+ ## Use labagent from plain English
84
+
85
+ A small local model turns a sentence into a command. You see the command and confirm it before anything runs.
86
+
87
+ ```bash
88
+ ollama pull qwen2.5-coder:0.5b
89
+ labagent "deploy rag-sec on databricks"
90
+ ```
91
+
92
+ Any OpenAI-compatible endpoint works. See [docs/packs.md](docs/packs.md#plain-english).
93
+
94
+ ## AWS sandboxes for workshops
95
+
96
+ labagent creates IAM users, a sandbox policy and a Lambda role per cohort with Terraform. Passwords are returned encrypted. It also tears down what learners created and cleans an untagged shared account from an explicit ruleset.
97
+
98
+ | Command | What it does |
99
+ |---|---|
100
+ | `provision` | Create a cohort's IAM layer with Terraform |
101
+ | `inventory`, `sweep` | List and delete what learners created. Plan first, then apply that reviewed plan |
102
+ | `reset`, `offboard` | Rotate a password. Destroy a cohort |
103
+ | `audit`, `cleanup` | Clean a long-lived account from a ruleset ([docs](docs/account-cleanup.md)) |
104
+
105
+ ```bash
106
+ labagent --config lab.json run --job job.json --output result.json
107
+ ```
108
+
109
+ Job format: [docs/job-contract.md](docs/job-contract.md). State and secrets: [docs/state-and-secrets.md](docs/state-and-secrets.md).
110
+
111
+ ## Safety
112
+
113
+ - Plan first, then apply. The plan is bound to the account, its items, an age limit and the hash a human approved.
114
+ - Only a `delete` rule can delete. A `keep` rule always wins. Unmatched resources are reported, never removed.
115
+ - labagent refuses to run in an AWS account that is not on your allow list.
116
+ - No plaintext secrets or emails in output or logs. No personal data in this repo. Tests enforce both.
117
+
118
+ Early release. Try labs in a throwaway account or workspace first. Apache-2.0. See [SECURITY.md](SECURITY.md) and [CONTRIBUTING.md](CONTRIBUTING.md).
119
+
120
+ Running labs for a class or team? Private batches and dashboard: https://becloudready.com
@@ -0,0 +1,82 @@
1
+ # labagent
2
+
3
+ labagent is an open-source CLI that launches hands-on labs for RAG, LLM apps and Databricks, then removes them so you do not pay for leftovers. It also sets up and cleans AWS training sandboxes with Terraform.
4
+
5
+ ## Quick start
6
+
7
+ ```bash
8
+ pip install 'labagent[rag]' # Python 3.9+; labs use the Databricks CLI: databricks auth login
9
+ labagent packs # list the labs
10
+ labagent launch rag-sec-filings --name demo
11
+ labagent destroy rag-sec-filings --name demo
12
+ ```
13
+
14
+ Every step is safe to re-run. `launch` resumes where it stopped. `destroy` removes only what the lab created.
15
+
16
+ ## Lab catalog
17
+
18
+ | Lab | What you learn | Cost | Command |
19
+ |---|---|---|---|
20
+ | `rag-sec-filings` | RAG over real SEC 10-K filings: chunking, hybrid search, citations, a quality gate before deploy | Databricks Free Edition | `labagent launch rag-sec-filings --name demo` |
21
+ | `rag-lakebase` | The same RAG pipeline on a small demo corpus, to smoke-test your setup | Databricks Free Edition | `labagent launch rag-lakebase --name demo` |
22
+ | `rag-sec-filings-live` | Fetch fresh filings from SEC EDGAR at launch | Databricks Free Edition | `labagent launch rag-sec-filings-live --name demo --param "user_agent=Your Name you@example.com"` |
23
+ | `db-agent` | Deploy [db-agent](https://github.com/db-agent/db-agent), a text-to-SQL agent, over a Databricks sample dataset | Databricks Free Edition | `labagent launch db-agent --name demo` |
24
+
25
+ Free Edition limits apply: one Lakebase project, three apps, and apps stop after 24 hours. Details: [docs/packs.md](docs/packs.md).
26
+
27
+ ## Run a RAG lab on Databricks Lakebase
28
+
29
+ `rag-sec-filings` builds a RAG app on Postgres with pgvector. It loads a pinned snapshot of 10-K filings, runs a retrieval quality check, and deploys the app only if the check passes. The app has company and year filters, suggested questions and cited answers.
30
+
31
+ ```bash
32
+ labagent launch rag-sec-filings --name demo
33
+ ```
34
+
35
+ The data comes from a checksummed release in [labagent-datasets](https://github.com/becloudready/labagent-datasets). Known gaps are listed in its README.
36
+
37
+ ## Text-to-SQL agent on Databricks
38
+
39
+ `db-agent` clones [github.com/db-agent/db-agent](https://github.com/db-agent/db-agent) at a pinned commit and deploys it to Databricks Apps. It then asks a test question through the live app. The data is the `samples` catalog every workspace has.
40
+
41
+ ```bash
42
+ labagent launch db-agent --name demo --param dataset=bakehouse
43
+ ```
44
+
45
+ ## Use labagent from plain English
46
+
47
+ A small local model turns a sentence into a command. You see the command and confirm it before anything runs.
48
+
49
+ ```bash
50
+ ollama pull qwen2.5-coder:0.5b
51
+ labagent "deploy rag-sec on databricks"
52
+ ```
53
+
54
+ Any OpenAI-compatible endpoint works. See [docs/packs.md](docs/packs.md#plain-english).
55
+
56
+ ## AWS sandboxes for workshops
57
+
58
+ labagent creates IAM users, a sandbox policy and a Lambda role per cohort with Terraform. Passwords are returned encrypted. It also tears down what learners created and cleans an untagged shared account from an explicit ruleset.
59
+
60
+ | Command | What it does |
61
+ |---|---|
62
+ | `provision` | Create a cohort's IAM layer with Terraform |
63
+ | `inventory`, `sweep` | List and delete what learners created. Plan first, then apply that reviewed plan |
64
+ | `reset`, `offboard` | Rotate a password. Destroy a cohort |
65
+ | `audit`, `cleanup` | Clean a long-lived account from a ruleset ([docs](docs/account-cleanup.md)) |
66
+
67
+ ```bash
68
+ labagent --config lab.json run --job job.json --output result.json
69
+ ```
70
+
71
+ Job format: [docs/job-contract.md](docs/job-contract.md). State and secrets: [docs/state-and-secrets.md](docs/state-and-secrets.md).
72
+
73
+ ## Safety
74
+
75
+ - Plan first, then apply. The plan is bound to the account, its items, an age limit and the hash a human approved.
76
+ - Only a `delete` rule can delete. A `keep` rule always wins. Unmatched resources are reported, never removed.
77
+ - labagent refuses to run in an AWS account that is not on your allow list.
78
+ - No plaintext secrets or emails in output or logs. No personal data in this repo. Tests enforce both.
79
+
80
+ Early release. Try labs in a throwaway account or workspace first. Apache-2.0. See [SECURITY.md](SECURITY.md) and [CONTRIBUTING.md](CONTRIBUTING.md).
81
+
82
+ Running labs for a class or team? Private batches and dashboard: https://becloudready.com
@@ -0,0 +1,111 @@
1
+ # Lab packs
2
+
3
+ A pack is one lab: ordered, idempotent steps behind `launch` and `destroy`. Packs run on the learner's machine with their own
4
+ credentials, or in your CI. Add a lab by writing a `Pack` subclass; the framework, CLI and result envelope do not change.
5
+
6
+ ```bash
7
+ labagent packs # list
8
+ labagent launch rag-lakebase --name demo --dry-run
9
+ labagent launch rag-lakebase --name demo # re-run to resume after a failure
10
+ labagent destroy rag-lakebase --name demo # removes only what this pack created
11
+ ```
12
+
13
+ ## Writing a pack
14
+
15
+ ```python
16
+ from labagent.packs.base import Pack, Param, Step
17
+
18
+ class MyLab(Pack):
19
+ id = "my-lab"; summary = "what it builds"
20
+ params = [Param("size", "small")]
21
+ def deploy_steps(self, ctx):
22
+ return [Step("make", "Create the thing", lambda c: {"thing_id": "..."}, check=lambda c: "thing_id" in c.state)]
23
+ def destroy_steps(self, ctx): ...
24
+ ```
25
+
26
+ Ship it in your own package and register it: `[project.entry-points."labagent.packs"] my-lab = "mypkg:MyLab"`.
27
+ Rules: `check` is a read-only "already done" test; step outputs are non-secret; tokens stay in memory (`ctx.mem`).
28
+ State is a 0600 file at `~/.labagent/packs/<pack>/<name>.json`.
29
+
30
+ ## RAG packs (Databricks Lakebase)
31
+
32
+ One reusable pipeline, pluggable data sources. Each stage is idempotent, and `evaluate` gates the deploy:
33
+
34
+ `preflight, project, locate, build (fetch, parse, chunk), schema, load (embed), index, evaluate, app`
35
+
36
+ | Pack | Source | Use |
37
+ |---|---|---|
38
+ | `rag-sec-filings` | Pinned snapshot of real SEC 10-Ks ([dataset release](https://github.com/becloudready/labagent-datasets/releases/tag/sec-10k-2026.10)), split into Items. No SEC access or contact needed | Financial research |
39
+ | `rag-sec-filings-live` | The same corpus downloaded fresh from EDGAR at launch (needs a contact for the SEC) | Newer filings, other tickers |
40
+ | `rag-lakebase` | 25 invented passages | Smoke test only |
41
+
42
+ ```bash
43
+ pip install 'labagent[rag]' # needs the Databricks CLI, then: databricks auth login
44
+ labagent launch rag-sec-filings --name sec --param "user_agent=Full Name name@example.com" \
45
+ --param tickers=AAPL,JPM,PFE --param filings=2
46
+ ```
47
+
48
+ What makes it more than a toy: section-aware chunking, hybrid retrieval (pgvector + full-text, rank-fused), metadata filters
49
+ (`{"ticker": "AAPL"}`), numbered citations with the filing URL, and an evaluation set that must reach `min_hit_rate`
50
+ (default 0.6) before the app is deployed. The snapshot is downloaded from a GitHub release and verified against a SHA-256 committed in
51
+ `labagent/packs/rag/sources/datasets.json`; a mismatch is refused. The live pack follows EDGAR rules (declared contact, under 10 requests
52
+ per second, cached locally). Filings are never stored in this repo.
53
+
54
+ Free Edition allows one Lakebase project: pass `--param project=<existing>` to share it. Each source uses its own schema, and
55
+ `destroy` drops only that schema (and the project only if labagent created it). Apps stop after 24 hours.
56
+
57
+ Add a source by subclassing `labagent.packs.rag.sources.Source` (`chunks()` and `eval_set()`) and registering it under the
58
+ `labagent.rag_sources` entry point.
59
+
60
+ ## Publishing a dataset snapshot (maintainers)
61
+
62
+ ```bash
63
+ labagent dataset --chunks ~/.labagent/cache/rag-sec-filings-live/<name>.jsonl --name sec-10k --version 2026.11 --out out/
64
+ gh release create sec-10k-2026.11 out/* --repo becloudready/labagent-datasets
65
+ ```
66
+
67
+ The build is byte-for-byte reproducible. Then add the release URL and the printed `sha256` to `datasets.json` in a pull request.
68
+ Data limitations (for example, no Item 7 for JPM and CVX) are listed in the dataset README.
69
+
70
+ ## db-agent (text-to-SQL on Databricks)
71
+
72
+ Deploys [db-agent](https://github.com/db-agent/db-agent), the open-source text-to-SQL agent, to Databricks Apps in your own workspace.
73
+ labagent contains none of db-agent's code: it clones the repo at a pinned commit, writes an `app.yaml` for your workspace, and deploys it.
74
+
75
+ ```bash
76
+ labagent launch db-agent --name demo # app: db-agent-demo, data: samples.bakehouse
77
+ labagent launch db-agent --name demo --param dataset=tpch # any schema in the samples catalog
78
+ labagent destroy db-agent --name demo
79
+ ```
80
+
81
+ Steps: `preflight` (login, SQL warehouse, sample data, serving endpoints), `fetch` (pinned commit), `token`, `app`, `verify`
82
+ (asks a real question through the deployed app: this proves the token, warehouse, LLM and SQL safety layer work together).
83
+
84
+ - **Pinned code.** `ref` must be a full commit SHA, and the clone is refused if HEAD differs. To move to a newer db-agent, review it and change the pin.
85
+ - **Data.** The `samples` catalog already exists in every workspace, so nothing is downloaded or redistributed.
86
+ - **Credential.** The app needs a token for the warehouse and the LLM. labagent creates one with a 30-day lifetime (`--param token_days`), stores it
87
+ only as an app secret (via stdin, never on a command line, in state or in output), and `destroy` revokes it. If your workspace blocks
88
+ token creation, create one yourself and `export LABAGENT_DATABRICKS_TOKEN=...`; labagent then uses it and never revokes it.
89
+ - **Free Edition.** Up to 3 apps per account, and apps stop after 24 hours.
90
+
91
+ ## Plain English
92
+
93
+ ```bash
94
+ labagent "Run and deploy rag-sec in databricks" # shows the command, asks, then runs it
95
+ labagent "tear down rag-sec" --print-only # just show the command
96
+ labagent ask what can you deploy # 'ask' lets you skip the quotes
97
+ ```
98
+
99
+ A small model reads the sentence and picks an action (launch, destroy, list) and a pack. By default that is `qwen2.5-coder:0.5b`
100
+ (about 400 MB) on local Ollama, so it runs on any laptop: `ollama pull qwen2.5-coder:0.5b`.
101
+ Any OpenAI-compatible endpoint works: `--endpoint`, `--model`, or `LABAGENT_LLM_URL`, `LABAGENT_LLM_MODEL`, `LABAGENT_LLM_API_KEY`.
102
+ A remote endpoint is announced before your sentence is sent to it.
103
+
104
+ The model never runs anything, and it is not trusted for details:
105
+ - The instance name (`called x`) and parameters (`profile DEFAULT`, `tickers AAPL,JPM`) are read from your sentence by exact rules, so they cannot be invented.
106
+ - A pack you typed (`rag-sec`, or any id or alias) wins over the model's pick. If the model guessed, the pack needs a distinctive word from your sentence.
107
+ - Requests that ask to create and delete in one sentence are refused. A delete needs a delete word, and you confirm by typing the instance name.
108
+ - Without a terminal, nothing runs unless you pass `--yes`.
109
+
110
+ Measured on `qwen2.5-coder:0.5b` with 42 phrasings (25 used while tuning, 17 unseen): all handled correctly, including refusals.
111
+ That is a small, author-written test set, so expect misses on unusual wording. A refusal is the usual failure, and the command shown is always checked by you.
@@ -0,0 +1,3 @@
1
+ """labagent: launch and tear down hands-on labs (RAG, LLM, Databricks) and AWS training sandboxes."""
2
+
3
+ __version__ = "0.2.0"
@@ -0,0 +1,154 @@
1
+ """labagent command line. stdout is silent unless --output - is given; logs go to stderr (ids and counts only)."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ import boto3
10
+
11
+ from . import __version__
12
+ from .config import STATE_BACKEND_DIR, load_config
13
+ from .context import Ctx, GuardError
14
+ from .envelope import Envelope
15
+ from .run import COMMANDS, run_job
16
+
17
+
18
+ def _emit(env: Envelope, output: str) -> None:
19
+ text = json.dumps(env.to_dict(), indent=2)
20
+ if output == "-":
21
+ print(text)
22
+ else:
23
+ Path(output).write_text(text)
24
+ Path(output).chmod(0o600)
25
+
26
+
27
+ def _packs_main(args) -> int:
28
+ from .packs import registry
29
+ from .packs.base import run_pack
30
+ if args.cmd == "packs":
31
+ for pid, pk in sorted(registry.all_packs().items()):
32
+ print(f"{pid:16} {pk.summary}")
33
+ return 0
34
+ try:
35
+ pack = registry.resolve(args.pack)
36
+ given = dict(kv.split("=", 1) for kv in args.param)
37
+ except (KeyError, ValueError) as e:
38
+ print(f"error: {e.args[0] if e.args else e}", file=sys.stderr)
39
+ return 2
40
+ name = args.name or pack.default_name
41
+ if not name:
42
+ print("error: --name is required for this pack", file=sys.stderr)
43
+ return 2
44
+ env = run_pack(pack, args.cmd, name, given, dry_run=args.dry_run)
45
+ _emit(env, args.output)
46
+ return env.exit_code
47
+
48
+
49
+ KNOWN = {"run", "packs", "dataset", "launch", "destroy", "bootstrap-path", "commands"}
50
+
51
+
52
+ def _nl_main(argv) -> int:
53
+ """labagent "deploy rag-sec on databricks": translate with a small model, confirm, then run the real command."""
54
+ from . import nl
55
+ from .packs import registry
56
+ q = argparse.ArgumentParser(prog="labagent", description="Plain-English front end: labagent \"deploy rag-sec on databricks\"")
57
+ q.add_argument("text", nargs="+")
58
+ q.add_argument("--endpoint", help=f"OpenAI-compatible base URL (default {nl.DEFAULT_URL}, or LABAGENT_LLM_URL)")
59
+ q.add_argument("--model", help=f"Model (default {nl.DEFAULT_MODEL}, or LABAGENT_LLM_MODEL)")
60
+ q.add_argument("-y", "--yes", action="store_true", help="Run without asking for confirmation")
61
+ q.add_argument("--print-only", action="store_true", help="Show the translated command and stop")
62
+ a = q.parse_args(argv[1:] if argv and argv[0] == "ask" else argv)
63
+ cfg = nl.settings(a.endpoint, a.model)
64
+ if not nl.is_local(cfg["url"]):
65
+ print(f"note: your request is sent to {cfg['url']}", file=sys.stderr)
66
+ try:
67
+ plan = nl.translate(" ".join(a.text), registry.all_packs(), registry.resolve, cfg["url"], cfg["model"], cfg["api_key"])
68
+ except nl.NLError as e:
69
+ print(f"error: {e}", file=sys.stderr)
70
+ return 2
71
+ for w in plan.warnings:
72
+ print(f"warning: {w}", file=sys.stderr)
73
+ print(f"Interpreted as:\n {plan.command_line()}", file=sys.stderr)
74
+ if a.print_only:
75
+ return 0
76
+ if not a.yes:
77
+ if not sys.stdin.isatty():
78
+ print("error: not an interactive terminal: re-run with --yes to execute, or use the command above", file=sys.stderr)
79
+ return 2
80
+ if plan.action == "destroy":
81
+ ans = input(f"This deletes resources. Type the instance name ({plan.name}) to confirm: ").strip()
82
+ if ans != plan.name:
83
+ print("cancelled", file=sys.stderr)
84
+ return 1
85
+ elif input("Run it? [y/N] ").strip().lower() not in ("y", "yes"):
86
+ print("cancelled", file=sys.stderr)
87
+ return 1
88
+ return main(plan.argv())
89
+
90
+
91
+ def main(argv=None) -> int:
92
+ argv = list(sys.argv[1:] if argv is None else argv)
93
+ if argv and not argv[0].startswith("-") and (argv[0] not in KNOWN or argv[0] == "ask"):
94
+ return _nl_main(argv)
95
+ p = argparse.ArgumentParser(prog="labagent", description=__doc__)
96
+ p.add_argument("--version", action="version", version=__version__)
97
+ p.add_argument("--config", help="JSON config file (or LABAGENT_CONFIG)")
98
+ p.add_argument("--profile", help="AWS profile for local use. CI uses OIDC environment credentials.")
99
+ sub = p.add_subparsers(dest="cmd", required=True)
100
+ r = sub.add_parser("run", help="Execute a job document: the CI entry point")
101
+ r.add_argument("--job", required=True, help="Path to job JSON (contains roster: keep it out of git and logs)")
102
+ r.add_argument("--output", default="result.json", help="Result envelope path, or - for stdout")
103
+ r.add_argument("--ruleset", help="Path to a ruleset JSON for audit/cleanup (merged into the job)")
104
+ sub.add_parser("packs", help="List available lab packs")
105
+ dp = sub.add_parser("dataset", help="Package a chunk file as a versioned, checksummed snapshot (maintainers)")
106
+ dp.add_argument("--chunks", required=True, help="JSONL of chunks, e.g. ~/.labagent/cache/rag-sec-filings/<name>.jsonl")
107
+ dp.add_argument("--name", required=True)
108
+ dp.add_argument("--version", required=True)
109
+ dp.add_argument("--out", required=True, help="Output directory")
110
+ for verb, text in (("launch", "Deploy a lab pack (idempotent: re-run to resume)"), ("destroy", "Remove what a pack created")):
111
+ lp = sub.add_parser(verb, help=text)
112
+ lp.add_argument("pack")
113
+ lp.add_argument("--name", help="Instance name (state key and resource suffix); defaults to the pack's own default")
114
+ lp.add_argument("--param", action="append", default=[], metavar="KEY=VALUE", help="Pack parameter, repeatable")
115
+ lp.add_argument("--dry-run", action="store_true", help="Show the steps without running them")
116
+ lp.add_argument("--output", default="-", help="Result envelope path, or - for stdout")
117
+ sub.add_parser("bootstrap-path", help="Print the path of the packaged Terraform state-backend module")
118
+ sub.add_parser("commands", help="List supported job commands")
119
+ args = p.parse_args(argv)
120
+
121
+ if args.cmd == "bootstrap-path":
122
+ print(STATE_BACKEND_DIR)
123
+ return 0
124
+ if args.cmd == "dataset":
125
+ from .datasets import DatasetError, build_snapshot
126
+ try:
127
+ m = build_snapshot(Path(args.chunks), args.name, args.version, Path(args.out))
128
+ except DatasetError as e:
129
+ print(f"error: {e}", file=sys.stderr)
130
+ return 2
131
+ print(json.dumps({k: m[k] for k in ("file", "chunks", "bytes", "sha256")}, indent=2))
132
+ return 0
133
+ if args.cmd in ("packs", "launch", "destroy"):
134
+ return _packs_main(args)
135
+ if args.cmd == "commands":
136
+ print("\n".join(sorted(COMMANDS)))
137
+ return 0
138
+ try:
139
+ cfg = load_config(args.config)
140
+ job = json.loads(Path(args.job).read_text())
141
+ session = boto3.Session(profile_name=args.profile, region_name=cfg.region)
142
+ if getattr(args, "ruleset", None) and "ruleset" not in job:
143
+ job["ruleset"] = json.loads(Path(args.ruleset).read_text())
144
+ env = run_job(Ctx(cfg, session), job)
145
+ except GuardError as e:
146
+ env = Envelope(command="guard").fail(str(e))
147
+ except Exception as e:
148
+ env = Envelope(command="startup").fail(f"{type(e).__name__}: {str(e)[:200]}")
149
+ _emit(env, args.output)
150
+ return env.exit_code
151
+
152
+
153
+ if __name__ == "__main__":
154
+ sys.exit(main())
@@ -0,0 +1,101 @@
1
+ """Versioned, checksum-pinned dataset snapshots.
2
+
3
+ A snapshot is a gzip'd JSONL of chunks (id, title, content, meta) plus a manifest. Snapshots are published as GitHub
4
+ release assets; the pinned URL and SHA-256 live in `labagent/packs/rag/sources/datasets.json`, committed with the code,
5
+ so changing the data is a reviewed code change. A download whose hash differs is refused and deleted.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import gzip
10
+ import hashlib
11
+ import json
12
+ import os
13
+ import urllib.request
14
+ from datetime import date
15
+ from pathlib import Path
16
+ from typing import Any, Callable, Dict, List, Optional
17
+
18
+ from . import __version__
19
+
20
+
21
+ class DatasetError(Exception):
22
+ pass
23
+
24
+
25
+ def sha256_file(path: Path) -> str:
26
+ h = hashlib.sha256()
27
+ with open(path, "rb") as f:
28
+ for block in iter(lambda: f.read(1 << 20), b""):
29
+ h.update(block)
30
+ return h.hexdigest()
31
+
32
+
33
+ def build_snapshot(chunks_path: Path, name: str, version: str, out_dir: Path, source: str = "SEC EDGAR",
34
+ built: Optional[str] = None) -> Dict[str, Any]:
35
+ """Write <name>-<version>.chunks.jsonl.gz (byte-for-byte reproducible), a manifest and SHA256SUMS."""
36
+ rows = [json.loads(l) for l in Path(chunks_path).read_text().splitlines() if l.strip()]
37
+ if not rows:
38
+ raise DatasetError("no chunks to package")
39
+ ids = [r["id"] for r in rows]
40
+ if len(ids) != len(set(ids)):
41
+ raise DatasetError("duplicate chunk ids")
42
+ out_dir.mkdir(parents=True, exist_ok=True)
43
+ stem = f"{name}-{version}"
44
+ data = out_dir / f"{stem}.chunks.jsonl.gz"
45
+ body = "\n".join(json.dumps(r, ensure_ascii=False, sort_keys=True, separators=(",", ":")) for r in rows) + "\n"
46
+ with open(data, "wb") as raw, gzip.GzipFile(fileobj=raw, mode="wb", mtime=0, compresslevel=9) as gz:
47
+ gz.write(body.encode("utf-8"))
48
+ filings = {}
49
+ for r in rows:
50
+ m = r["meta"]
51
+ if "url" in m:
52
+ filings[m["url"]] = {k: m.get(k) for k in ("ticker", "company", "form", "fiscal_year", "filed", "url")}
53
+ manifest = {
54
+ "name": name, "version": version, "source": source, "built": built or date.today().isoformat(),
55
+ "labagent": __version__, "chunks": len(rows), "file": data.name, "sha256": sha256_file(data), "bytes": data.stat().st_size,
56
+ "companies": sorted({f["ticker"] for f in filings.values() if f.get("ticker")}),
57
+ "filings": sorted(filings.values(), key=lambda f: (f["ticker"], f["fiscal_year"])),
58
+ "items": sorted({r["meta"].get("item") for r in rows if r["meta"].get("item")}),
59
+ }
60
+ (out_dir / f"{stem}.manifest.json").write_text(json.dumps(manifest, indent=2, sort_keys=True) + "\n")
61
+ (out_dir / "SHA256SUMS").write_text(f"{manifest['sha256']} {data.name}\n")
62
+ return manifest
63
+
64
+
65
+ def registry() -> Dict[str, Dict[str, Any]]:
66
+ return json.loads((Path(__file__).parent / "packs" / "rag" / "sources" / "datasets.json").read_text())
67
+
68
+
69
+ def default_http(url: str, dest: Path) -> None:
70
+ req = urllib.request.Request(url, headers={"User-Agent": f"labagent/{__version__}"})
71
+ with urllib.request.urlopen(req, timeout=120) as r, open(dest, "wb") as f:
72
+ for block in iter(lambda: r.read(1 << 20), b""):
73
+ f.write(block)
74
+
75
+
76
+ def fetch_snapshot(name: str, cache_dir: Optional[Path] = None, download: Callable[[str, Path], None] = default_http,
77
+ entry: Optional[Dict[str, Any]] = None) -> List[Dict[str, Any]]:
78
+ """Return the chunks of a pinned snapshot, downloading once and verifying the SHA-256 every time it is read."""
79
+ entry = entry or registry().get(name)
80
+ if not entry:
81
+ raise DatasetError(f"unknown dataset '{name}'. Known: {', '.join(sorted(registry()))}")
82
+ root = cache_dir or Path(os.environ.get("LABAGENT_HOME") or Path.home() / ".labagent") / "cache" / "datasets"
83
+ root.mkdir(parents=True, exist_ok=True)
84
+ dest = root / f"{entry['sha256'][:16]}-{Path(entry['url']).name}"
85
+ if not dest.exists() or sha256_file(dest) != entry["sha256"]:
86
+ errors = []
87
+ for url in [entry["url"], *entry.get("mirrors", [])]:
88
+ tmp = dest.with_suffix(".part")
89
+ try:
90
+ download(url, tmp)
91
+ if sha256_file(tmp) != entry["sha256"]:
92
+ raise DatasetError("checksum mismatch")
93
+ tmp.replace(dest)
94
+ break
95
+ except Exception as e:
96
+ tmp.unlink(missing_ok=True)
97
+ errors.append(f"{url.split('/')[2]}: {type(e).__name__}")
98
+ else:
99
+ raise DatasetError(f"could not obtain dataset '{name}' with a matching checksum ({'; '.join(errors)})")
100
+ with gzip.open(dest, "rt", encoding="utf-8") as f:
101
+ return [json.loads(l) for l in f if l.strip()]