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.
- labagent-0.2.0/PKG-INFO +120 -0
- labagent-0.2.0/README.md +82 -0
- labagent-0.2.0/docs/packs.md +111 -0
- labagent-0.2.0/labagent/__init__.py +3 -0
- labagent-0.2.0/labagent/cli.py +154 -0
- labagent-0.2.0/labagent/datasets.py +101 -0
- labagent-0.2.0/labagent/nl.py +246 -0
- labagent-0.2.0/labagent/packs/__init__.py +5 -0
- labagent-0.2.0/labagent/packs/base.py +144 -0
- labagent-0.2.0/labagent/packs/databricks/__init__.py +2 -0
- labagent-0.2.0/labagent/packs/databricks/auth.py +38 -0
- labagent-0.2.0/labagent/packs/databricks/cli.py +73 -0
- labagent-0.2.0/labagent/packs/databricks/pg.py +34 -0
- labagent-0.2.0/labagent/packs/db_agent/pack.py +269 -0
- labagent-0.2.0/labagent/packs/gitsrc.py +45 -0
- labagent-0.2.0/labagent/packs/rag/__init__.py +0 -0
- labagent-0.2.0/labagent/packs/rag/app/app.py +106 -0
- labagent-0.2.0/labagent/packs/rag/app/app.yaml +16 -0
- labagent-0.2.0/labagent/packs/rag/app/requirements.txt +4 -0
- labagent-0.2.0/labagent/packs/rag/app/retrieval.py +45 -0
- labagent-0.2.0/labagent/packs/rag/app/static/index.html +137 -0
- labagent-0.2.0/labagent/packs/rag/chunking.py +49 -0
- labagent-0.2.0/labagent/packs/rag/pack.py +329 -0
- labagent-0.2.0/labagent/packs/rag/sources/__init__.py +53 -0
- labagent-0.2.0/labagent/packs/rag/sources/builtin.py +41 -0
- labagent-0.2.0/labagent/packs/rag/sources/builtin_corpus.jsonl +25 -0
- labagent-0.2.0/labagent/packs/rag/sources/sec.py +237 -0
- labagent-0.2.0/labagent/packs/registry.py +49 -0
- labagent-0.2.0/labagent.egg-info/PKG-INFO +120 -0
- {labagent-0.1.0 → labagent-0.2.0}/labagent.egg-info/SOURCES.txt +31 -0
- {labagent-0.1.0 → labagent-0.2.0}/labagent.egg-info/requires.txt +3 -0
- {labagent-0.1.0 → labagent-0.2.0}/pyproject.toml +6 -2
- labagent-0.2.0/tests/__init__.py +0 -0
- labagent-0.2.0/tests/test_datasets.py +73 -0
- labagent-0.2.0/tests/test_db_agent.py +259 -0
- labagent-0.2.0/tests/test_nl.py +207 -0
- labagent-0.2.0/tests/test_packs.py +369 -0
- labagent-0.2.0/tests/test_rag_app.py +109 -0
- labagent-0.2.0/tests/test_rag_sec.py +169 -0
- labagent-0.1.0/PKG-INFO +0 -73
- labagent-0.1.0/README.md +0 -37
- labagent-0.1.0/labagent/__init__.py +0 -3
- labagent-0.1.0/labagent/cli.py +0 -63
- labagent-0.1.0/labagent.egg-info/PKG-INFO +0 -73
- {labagent-0.1.0 → labagent-0.2.0}/CONTRIBUTING.md +0 -0
- {labagent-0.1.0 → labagent-0.2.0}/LICENSE +0 -0
- {labagent-0.1.0 → labagent-0.2.0}/MANIFEST.in +0 -0
- {labagent-0.1.0 → labagent-0.2.0}/SECURITY.md +0 -0
- {labagent-0.1.0 → labagent-0.2.0}/docs/account-cleanup.md +0 -0
- {labagent-0.1.0 → labagent-0.2.0}/docs/e2e-test-plan.md +0 -0
- {labagent-0.1.0 → labagent-0.2.0}/docs/examples/oidc-trust-policy.example.json +0 -0
- {labagent-0.1.0 → labagent-0.2.0}/docs/examples/permissions-read.example.json +0 -0
- {labagent-0.1.0 → labagent-0.2.0}/docs/examples/permissions-write.example.json +0 -0
- {labagent-0.1.0 → labagent-0.2.0}/docs/examples/runner-workflow.example.yml +0 -0
- {labagent-0.1.0 → labagent-0.2.0}/docs/job-contract.md +0 -0
- {labagent-0.1.0 → labagent-0.2.0}/docs/state-and-secrets.md +0 -0
- {labagent-0.1.0 → labagent-0.2.0}/examples/config.example.json +0 -0
- {labagent-0.1.0 → labagent-0.2.0}/examples/rulesets/legacy-cleanup.example.json +0 -0
- {labagent-0.1.0 → labagent-0.2.0}/labagent/audit.py +0 -0
- {labagent-0.1.0 → labagent-0.2.0}/labagent/cleanup.py +0 -0
- {labagent-0.1.0 → labagent-0.2.0}/labagent/config.py +0 -0
- {labagent-0.1.0 → labagent-0.2.0}/labagent/context.py +0 -0
- {labagent-0.1.0 → labagent-0.2.0}/labagent/deleters.py +0 -0
- {labagent-0.1.0 → labagent-0.2.0}/labagent/discovery.py +0 -0
- {labagent-0.1.0 → labagent-0.2.0}/labagent/envelope.py +0 -0
- {labagent-0.1.0 → labagent-0.2.0}/labagent/iam_ops.py +0 -0
- {labagent-0.1.0/tests → labagent-0.2.0/labagent/packs/db_agent}/__init__.py +0 -0
- {labagent-0.1.0 → labagent-0.2.0}/labagent/plan.py +0 -0
- {labagent-0.1.0 → labagent-0.2.0}/labagent/rules.py +0 -0
- {labagent-0.1.0 → labagent-0.2.0}/labagent/run.py +0 -0
- {labagent-0.1.0 → labagent-0.2.0}/labagent/secrets_box.py +0 -0
- {labagent-0.1.0 → labagent-0.2.0}/labagent/sweep.py +0 -0
- {labagent-0.1.0 → labagent-0.2.0}/labagent/terraform/cohort_iam/.terraform.lock.hcl +0 -0
- {labagent-0.1.0 → labagent-0.2.0}/labagent/terraform/cohort_iam/main.tf +0 -0
- {labagent-0.1.0 → labagent-0.2.0}/labagent/terraform/cohort_iam/outputs.tf +0 -0
- {labagent-0.1.0 → labagent-0.2.0}/labagent/terraform/cohort_iam/sandbox-policy.json.tftpl +0 -0
- {labagent-0.1.0 → labagent-0.2.0}/labagent/terraform/cohort_iam/tests/cohort.tftest.hcl +0 -0
- {labagent-0.1.0 → labagent-0.2.0}/labagent/terraform/cohort_iam/variables.tf +0 -0
- {labagent-0.1.0 → labagent-0.2.0}/labagent/terraform/cohort_iam/versions.tf +0 -0
- {labagent-0.1.0 → labagent-0.2.0}/labagent/terraform/state_backend/.terraform.lock.hcl +0 -0
- {labagent-0.1.0 → labagent-0.2.0}/labagent/terraform/state_backend/main.tf +0 -0
- {labagent-0.1.0 → labagent-0.2.0}/labagent/terraform/state_backend/tests/bootstrap.tftest.hcl +0 -0
- {labagent-0.1.0 → labagent-0.2.0}/labagent/tf.py +0 -0
- {labagent-0.1.0 → labagent-0.2.0}/labagent.egg-info/dependency_links.txt +0 -0
- {labagent-0.1.0 → labagent-0.2.0}/labagent.egg-info/entry_points.txt +0 -0
- {labagent-0.1.0 → labagent-0.2.0}/labagent.egg-info/top_level.txt +0 -0
- {labagent-0.1.0 → labagent-0.2.0}/scripts/check_no_pii.py +0 -0
- {labagent-0.1.0 → labagent-0.2.0}/scripts/make_test_job.py +0 -0
- {labagent-0.1.0 → labagent-0.2.0}/setup.cfg +0 -0
- {labagent-0.1.0 → labagent-0.2.0}/tests/conftest.py +0 -0
- {labagent-0.1.0 → labagent-0.2.0}/tests/test_classify.py +0 -0
- {labagent-0.1.0 → labagent-0.2.0}/tests/test_cleanup.py +0 -0
- {labagent-0.1.0 → labagent-0.2.0}/tests/test_cloudfront_and_cli.py +0 -0
- {labagent-0.1.0 → labagent-0.2.0}/tests/test_iam_ops.py +0 -0
- {labagent-0.1.0 → labagent-0.2.0}/tests/test_plan.py +0 -0
- {labagent-0.1.0 → labagent-0.2.0}/tests/test_repo_hygiene.py +0 -0
- {labagent-0.1.0 → labagent-0.2.0}/tests/test_sweep_moto.py +0 -0
- {labagent-0.1.0 → labagent-0.2.0}/tests/test_tf_wrapper.py +0 -0
labagent-0.2.0/PKG-INFO
ADDED
|
@@ -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
|
labagent-0.2.0/README.md
ADDED
|
@@ -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,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()]
|