programasweights 0.2.8__tar.gz → 0.3.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 (61) hide show
  1. {programasweights-0.2.8 → programasweights-0.3.1}/AGENTS.md +25 -4
  2. {programasweights-0.2.8 → programasweights-0.3.1}/PKG-INFO +17 -14
  3. {programasweights-0.2.8 → programasweights-0.3.1}/PYPI_README.md +16 -13
  4. {programasweights-0.2.8 → programasweights-0.3.1}/docs/index.md +2 -2
  5. {programasweights-0.2.8 → programasweights-0.3.1}/programasweights/__init__.py +73 -16
  6. {programasweights-0.2.8 → programasweights-0.3.1}/programasweights/client.py +14 -0
  7. {programasweights-0.2.8 → programasweights-0.3.1}/pyproject.toml +1 -1
  8. {programasweights-0.2.8 → programasweights-0.3.1}/.gitignore +0 -0
  9. {programasweights-0.2.8 → programasweights-0.3.1}/.readthedocs.yaml +0 -0
  10. {programasweights-0.2.8 → programasweights-0.3.1}/CHANGELOG.md +0 -0
  11. {programasweights-0.2.8 → programasweights-0.3.1}/LICENSE +0 -0
  12. {programasweights-0.2.8 → programasweights-0.3.1}/README.md +0 -0
  13. {programasweights-0.2.8 → programasweights-0.3.1}/docs/adr/001-llama-cpp-over-pytorch.md +0 -0
  14. {programasweights-0.2.8 → programasweights-0.3.1}/docs/adr/002-q4_0-adapter-format.md +0 -0
  15. {programasweights-0.2.8 → programasweights-0.3.1}/docs/adr/003-single-spec-field.md +0 -0
  16. {programasweights-0.2.8 → programasweights-0.3.1}/docs/adr/004-compiler-naming.md +0 -0
  17. {programasweights-0.2.8 → programasweights-0.3.1}/docs/adr/005-vllm-hidden-states.md +0 -0
  18. {programasweights-0.2.8 → programasweights-0.3.1}/docs/adr/006-email-api-key-auth.md +0 -0
  19. {programasweights-0.2.8 → programasweights-0.3.1}/docs/advanced/adrs.md +0 -0
  20. {programasweights-0.2.8 → programasweights-0.3.1}/docs/advanced/architecture.md +0 -0
  21. {programasweights-0.2.8 → programasweights-0.3.1}/docs/api-reference/cli.md +0 -0
  22. {programasweights-0.2.8 → programasweights-0.3.1}/docs/api-reference/python-sdk.md +0 -0
  23. {programasweights-0.2.8 → programasweights-0.3.1}/docs/api-reference/rest-api.md +0 -0
  24. {programasweights-0.2.8 → programasweights-0.3.1}/docs/architecture.md +0 -0
  25. {programasweights-0.2.8 → programasweights-0.3.1}/docs/case-studies/log-monitoring.md +0 -0
  26. {programasweights-0.2.8 → programasweights-0.3.1}/docs/case-studies/semantic-search.md +0 -0
  27. {programasweights-0.2.8 → programasweights-0.3.1}/docs/case-studies/site-navigation.md +0 -0
  28. {programasweights-0.2.8 → programasweights-0.3.1}/docs/case-studies/tool-calling.md +0 -0
  29. {programasweights-0.2.8 → programasweights-0.3.1}/docs/getting-started/first-program.md +0 -0
  30. {programasweights-0.2.8 → programasweights-0.3.1}/docs/getting-started/installation.md +0 -0
  31. {programasweights-0.2.8 → programasweights-0.3.1}/docs/getting-started/naming-programs.md +0 -0
  32. {programasweights-0.2.8 → programasweights-0.3.1}/docs/guide/browser-inference.md +0 -0
  33. {programasweights-0.2.8 → programasweights-0.3.1}/docs/guide/how-it-works.md +0 -0
  34. {programasweights-0.2.8 → programasweights-0.3.1}/docs/guide/local-inference.md +0 -0
  35. {programasweights-0.2.8 → programasweights-0.3.1}/docs/guide/writing-good-specs.md +0 -0
  36. {programasweights-0.2.8 → programasweights-0.3.1}/docs/hub/browsing-programs.md +0 -0
  37. {programasweights-0.2.8 → programasweights-0.3.1}/docs/hub/feedback-cases.md +0 -0
  38. {programasweights-0.2.8 → programasweights-0.3.1}/docs/hub/publishing-programs.md +0 -0
  39. {programasweights-0.2.8 → programasweights-0.3.1}/docs/requirements.txt +0 -0
  40. {programasweights-0.2.8 → programasweights-0.3.1}/examples/flask_app.py +0 -0
  41. {programasweights-0.2.8 → programasweights-0.3.1}/examples/jupyter_notebook.py +0 -0
  42. {programasweights-0.2.8 → programasweights-0.3.1}/examples/langchain_integration.py +0 -0
  43. {programasweights-0.2.8 → programasweights-0.3.1}/examples/paw_monitor.py +0 -0
  44. {programasweights-0.2.8 → programasweights-0.3.1}/examples/replace_openai.py +0 -0
  45. {programasweights-0.2.8 → programasweights-0.3.1}/mkdocs.yml +0 -0
  46. {programasweights-0.2.8 → programasweights-0.3.1}/programasweights/_output.py +0 -0
  47. {programasweights-0.2.8 → programasweights-0.3.1}/programasweights/artifacts.py +0 -0
  48. {programasweights-0.2.8 → programasweights-0.3.1}/programasweights/cache.py +0 -0
  49. {programasweights-0.2.8 → programasweights-0.3.1}/programasweights/cli.py +0 -0
  50. {programasweights-0.2.8 → programasweights-0.3.1}/programasweights/compiler/__init__.py +0 -0
  51. {programasweights-0.2.8 → programasweights-0.3.1}/programasweights/compiler/dummy.py +0 -0
  52. {programasweights-0.2.8 → programasweights-0.3.1}/programasweights/config.py +0 -0
  53. {programasweights-0.2.8 → programasweights-0.3.1}/programasweights/convert_peft_to_paw.py +0 -0
  54. {programasweights-0.2.8 → programasweights-0.3.1}/programasweights/paw_format.py +0 -0
  55. {programasweights-0.2.8 → programasweights-0.3.1}/programasweights/runtime/__init__.py +0 -0
  56. {programasweights-0.2.8 → programasweights-0.3.1}/programasweights/runtime/interpreter.py +0 -0
  57. {programasweights-0.2.8 → programasweights-0.3.1}/programasweights/runtime/interpreter_onnx.py +0 -0
  58. {programasweights-0.2.8 → programasweights-0.3.1}/programasweights/runtime_llamacpp.py +0 -0
  59. {programasweights-0.2.8 → programasweights-0.3.1}/tests/test_offline_cache.py +0 -0
  60. {programasweights-0.2.8 → programasweights-0.3.1}/tests/test_sdk.py +0 -0
  61. {programasweights-0.2.8 → programasweights-0.3.1}/tests/test_sdk.sh +0 -0
@@ -20,7 +20,7 @@ import programasweights as paw
20
20
  # "email-triage" is an official pre-compiled program (slug)
21
21
  fn = paw.function("email-triage")
22
22
  fn("Urgent: server is down!") # "immediate"
23
- fn("Newsletter: spring picnic") # "can wait"
23
+ fn("Newsletter: spring picnic") # "wait"
24
24
 
25
25
  # Compile your own from a description
26
26
  program = paw.compile(
@@ -28,7 +28,7 @@ program = paw.compile(
28
28
  compiler="paw-4b-qwen3-0.6b" # or "paw-4b-gpt2" for smaller/faster
29
29
  )
30
30
  fn = paw.function(program.id)
31
- fn("{name: 'Alice',}") # '{"name": "Alice"}'
31
+ fn("{name: 'Alice',}") # '{"name":"Alice"}'
32
32
 
33
33
  # Or compile and load in one step
34
34
  fn = paw.compile_and_load("Classify sentiment as positive or negative")
@@ -161,6 +161,22 @@ Generate API keys at https://programasweights.com/settings.
161
161
 
162
162
  Commands: `paw compile --spec "..." --json`, `paw run --program <id> --input "..."`, `paw info <id>`, `paw rename <id> <slug>`, `paw login`. All support `--json` for structured output.
163
163
 
164
+ ## Versioning
165
+
166
+ Slugs support version history. Recompiling with the same slug creates a new version:
167
+
168
+ ```python
169
+ p1 = paw.compile("Count words v1", slug="word-counter") # v1
170
+ p2 = paw.compile("Count words v2", slug="word-counter") # v2 (auto-bumps)
171
+
172
+ fn = paw.function("da03/word-counter") # resolves to main (latest)
173
+ fn = paw.function("da03/word-counter@v1") # pinned to v1
174
+
175
+ versions = paw.list_versions("da03/word-counter") # all versions
176
+ ```
177
+
178
+ Pinned versions (`@v1`) are immutable and cached locally forever. Bare slugs always check the server for the latest main version (falls back to cache if offline).
179
+
164
180
  ## Full API Reference
165
181
 
166
182
  ```python
@@ -170,16 +186,19 @@ program = paw.compile(
170
186
  slug=None, # URL-safe handle (requires auth)
171
187
  public=True, # list on public hub
172
188
  )
173
- # Returns: Program(id, slug, status, timings, error)
189
+ # Returns: Program(id, slug, status, version, version_action, timings, error)
174
190
 
175
191
  fn = paw.function(program) # accepts Program object, hash ID, or slug
176
192
  fn = paw.function("a6b454023d41ac9ca845")
177
193
  fn = paw.function("da03/my-classifier")
194
+ fn = paw.function("da03/my-classifier@v2") # pinned version
195
+ fn = paw.function("da03/my-classifier", offline=True) # skip server check
178
196
 
179
197
  result: str = fn(input_text: str, max_tokens=None, temperature=0.0)
180
198
 
181
199
  fn = paw.compile_and_load(spec, compiler="paw-4b-qwen3-0.6b")
182
200
 
201
+ versions = paw.list_versions("da03/my-classifier") # version history
183
202
  programs = paw.list_programs(sort="recent", per_page=20) # requires auth
184
203
 
185
204
  paw.login()
@@ -192,10 +211,12 @@ paw.login()
192
211
  | `RuntimeError: assets not ready` on download | Program still generating after compile | SDK polls automatically for up to 30s. If persistent, recompile. |
193
212
  | `httpx.HTTPStatusError: 422` on compile | Spec too short (<10 chars) | Adjust spec length. |
194
213
  | `httpx.HTTPStatusError: 429` | Rate limit exceeded | Wait, or sign in for higher limits. |
214
+ | GPU/Metal errors on load | GPU backend not available or incompatible | Set `PAW_GPU_LAYERS=0` or pass `n_gpu_layers=0` to force CPU. |
195
215
 
196
216
  ## Performance
197
217
 
198
- - **First call** ~5-15s (loads base model). Subsequent calls 0.5-5s depending on input length.
218
+ - **GPU acceleration** enabled by default (`n_gpu_layers=-1`). Uses Metal on Mac, CUDA on Linux, falls back to CPU automatically. If GPU causes issues, set `PAW_GPU_LAYERS=0` or pass `n_gpu_layers=0`.
219
+ - **First call** ~1-5s (loads base model). Subsequent calls ~0.05-0.5s depending on input length and GPU availability.
199
220
  - **Base model shared** across functions on disk. Each LoRA adapter adds ~22 MB.
200
221
  - **Cache**: `~/.cache/programasweights/`. Override with `PAW_CACHE_DIR`.
201
222
  - **Offline** after first download.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: programasweights
3
- Version: 0.2.8
3
+ Version: 0.3.1
4
4
  Summary: Compile natural language specifications into neural programs that run locally via llama.cpp.
5
5
  Project-URL: Homepage, https://programasweights.com
6
6
  Project-URL: Repository, https://github.com/programasweights/programasweights-python
@@ -48,7 +48,7 @@ import programasweights as paw
48
48
  # Use a pre-compiled function (downloads once, runs locally forever)
49
49
  fn = paw.function("email-triage")
50
50
  fn("Urgent: the server is down!") # "immediate"
51
- fn("Newsletter: spring picnic") # "can wait"
51
+ fn("Newsletter: spring picnic") # "wait"
52
52
 
53
53
  # Compile your own from a description
54
54
  program = paw.compile(
@@ -57,7 +57,7 @@ program = paw.compile(
57
57
  slug="json-fixer" # optional: creates username/json-fixer handle
58
58
  )
59
59
  fn = paw.function(program.slug) # or paw.function(program.id)
60
- fn("{name: 'Alice',}") # '{"name": "Alice"}'
60
+ fn("{name: 'Alice',}") # '{"name":"Alice"}'
61
61
 
62
62
  # Or compile and load in one step
63
63
  fn = paw.compile_and_load("Classify sentiment as positive or negative")
@@ -66,17 +66,20 @@ fn("I love this!") # "positive"
66
66
 
67
67
  ## Two Compilers
68
68
 
69
- | | Standard (Qwen3 0.6B) | Compact (GPT-2 124M) |
70
- |--------------------|-------------------------|------------------------|
71
- | Compiler name | `paw-4b-qwen3-0.6b` | `paw-4b-gpt2` |
72
- | Accuracy | Higher | Lower |
73
- | Base model size | 594 MB | 105 MB |
74
- | Program size | ~22 MB | ~5 MB |
75
- | Inference speed | ~90ms (server) | ~50ms (server) |
76
- | Runs in browser | No | Yes |
69
+
70
+ | | Standard (Qwen3 0.6B) | Compact (GPT-2 124M) |
71
+ | --------------- | --------------------- | -------------------- |
72
+ | Compiler name | `paw-4b-qwen3-0.6b` | `paw-4b-gpt2` |
73
+ | Accuracy | Higher | Lower |
74
+ | Base model size | 594 MB | 105 MB |
75
+ | Program size | ~22 MB | ~5 MB |
76
+ | Local inference | ~0.05-0.5s per call | ~0.03-0.3s per call |
77
+ | Runs in browser | No | Yes (WebAssembly) |
77
78
 
78
79
  Default is Standard (Qwen3 0.6B). Use Compact (GPT-2) when you need smaller files or browser deployment.
79
80
 
81
+ GPU acceleration is enabled by default (Metal on Mac, CUDA on Linux, falls back to CPU). Set `PAW_GPU_LAYERS=0` to force CPU if GPU causes issues.
82
+
80
83
  ## Browser SDK
81
84
 
82
85
  Programs compiled with GPT-2 also run entirely in the browser via WebAssembly — no server needed, data never leaves the user's device.
@@ -99,9 +102,9 @@ See the [browser SDK repo](https://github.com/programasweights/programasweights-
99
102
 
100
103
  PAW works with Cursor, Claude, Codex, and other AI coding assistants. Paste this into your agent's chat:
101
104
 
102
- > I want to use ProgramAsWeights (PAW) to create fuzzy text functions that run locally. Read the instructions at https://programasweights.com/agents and help me integrate it.
105
+ > I want to use ProgramAsWeights (PAW) to create fuzzy text functions that run locally. Read the instructions at [https://programasweights.com/AGENTS.md](https://programasweights.com/AGENTS.md) and help me integrate it.
103
106
 
104
- Or save [`AGENTS.md`](https://programasweights.com/agents) to your project root — agents read it automatically.
107
+ Or save `[AGENTS.md](https://programasweights.com/agents)` to your project root — agents read it automatically.
105
108
 
106
109
  ## When to Use PAW
107
110
 
@@ -145,4 +148,4 @@ paw login
145
148
 
146
149
  ## License
147
150
 
148
- MIT
151
+ MIT
@@ -18,7 +18,7 @@ import programasweights as paw
18
18
  # Use a pre-compiled function (downloads once, runs locally forever)
19
19
  fn = paw.function("email-triage")
20
20
  fn("Urgent: the server is down!") # "immediate"
21
- fn("Newsletter: spring picnic") # "can wait"
21
+ fn("Newsletter: spring picnic") # "wait"
22
22
 
23
23
  # Compile your own from a description
24
24
  program = paw.compile(
@@ -27,7 +27,7 @@ program = paw.compile(
27
27
  slug="json-fixer" # optional: creates username/json-fixer handle
28
28
  )
29
29
  fn = paw.function(program.slug) # or paw.function(program.id)
30
- fn("{name: 'Alice',}") # '{"name": "Alice"}'
30
+ fn("{name: 'Alice',}") # '{"name":"Alice"}'
31
31
 
32
32
  # Or compile and load in one step
33
33
  fn = paw.compile_and_load("Classify sentiment as positive or negative")
@@ -36,17 +36,20 @@ fn("I love this!") # "positive"
36
36
 
37
37
  ## Two Compilers
38
38
 
39
- | | Standard (Qwen3 0.6B) | Compact (GPT-2 124M) |
40
- |--------------------|-------------------------|------------------------|
41
- | Compiler name | `paw-4b-qwen3-0.6b` | `paw-4b-gpt2` |
42
- | Accuracy | Higher | Lower |
43
- | Base model size | 594 MB | 105 MB |
44
- | Program size | ~22 MB | ~5 MB |
45
- | Inference speed | ~90ms (server) | ~50ms (server) |
46
- | Runs in browser | No | Yes |
39
+
40
+ | | Standard (Qwen3 0.6B) | Compact (GPT-2 124M) |
41
+ | --------------- | --------------------- | -------------------- |
42
+ | Compiler name | `paw-4b-qwen3-0.6b` | `paw-4b-gpt2` |
43
+ | Accuracy | Higher | Lower |
44
+ | Base model size | 594 MB | 105 MB |
45
+ | Program size | ~22 MB | ~5 MB |
46
+ | Local inference | ~0.05-0.5s per call | ~0.03-0.3s per call |
47
+ | Runs in browser | No | Yes (WebAssembly) |
47
48
 
48
49
  Default is Standard (Qwen3 0.6B). Use Compact (GPT-2) when you need smaller files or browser deployment.
49
50
 
51
+ GPU acceleration is enabled by default (Metal on Mac, CUDA on Linux, falls back to CPU). Set `PAW_GPU_LAYERS=0` to force CPU if GPU causes issues.
52
+
50
53
  ## Browser SDK
51
54
 
52
55
  Programs compiled with GPT-2 also run entirely in the browser via WebAssembly — no server needed, data never leaves the user's device.
@@ -69,9 +72,9 @@ See the [browser SDK repo](https://github.com/programasweights/programasweights-
69
72
 
70
73
  PAW works with Cursor, Claude, Codex, and other AI coding assistants. Paste this into your agent's chat:
71
74
 
72
- > I want to use ProgramAsWeights (PAW) to create fuzzy text functions that run locally. Read the instructions at https://programasweights.com/agents and help me integrate it.
75
+ > I want to use ProgramAsWeights (PAW) to create fuzzy text functions that run locally. Read the instructions at [https://programasweights.com/AGENTS.md](https://programasweights.com/AGENTS.md) and help me integrate it.
73
76
 
74
- Or save [`AGENTS.md`](https://programasweights.com/agents) to your project root — agents read it automatically.
77
+ Or save `[AGENTS.md](https://programasweights.com/agents)` to your project root — agents read it automatically.
75
78
 
76
79
  ## When to Use PAW
77
80
 
@@ -115,4 +118,4 @@ paw login
115
118
 
116
119
  ## License
117
120
 
118
- MIT
121
+ MIT
@@ -20,7 +20,7 @@ result = fn("Thesis defense committee needs your signature by EOD")
20
20
  print(result) # "immediate"
21
21
 
22
22
  result = fn("Department newsletter: spring picnic next Friday")
23
- print(result) # "can wait"
23
+ print(result) # "wait"
24
24
  ```
25
25
 
26
26
  ## Compile Your Own
@@ -35,7 +35,7 @@ program = paw.compile(
35
35
 
36
36
  # Use the compiled program
37
37
  fn = paw.function(program.id)
38
- fn("{name: 'Alice', age: 30,}") # '{"name": "Alice", "age": 30}'
38
+ fn("{name: 'Alice', age: 30,}") # '{"name":"Alice","age":30}'
39
39
  ```
40
40
 
41
41
  ## What Can You Build?
@@ -12,7 +12,7 @@ Quick start:
12
12
  # Compile your own from a description
13
13
  program = paw.compile("Fix malformed JSON: repair missing quotes and trailing commas")
14
14
  fn = paw.function(program.id)
15
- fn("{name: 'Alice',}") # '{"name": "Alice"}'
15
+ fn("{name: 'Alice',}") # '{"name":"Alice"}'
16
16
 
17
17
  API reference:
18
18
  paw.compile(spec) Compile a spec on the server, returns Program
@@ -27,7 +27,7 @@ try:
27
27
  from importlib.metadata import version as _meta_version
28
28
  __version__ = _meta_version("programasweights")
29
29
  except Exception:
30
- __version__ = "0.2.8"
30
+ __version__ = "0.3.1"
31
31
 
32
32
  from .config import get_api_url, get_api_key, set_api_key
33
33
 
@@ -65,7 +65,7 @@ def compile(
65
65
  ... )
66
66
  >>> fn = paw.function(program.slug) # or paw.function(program.id)
67
67
  >>> fn("{name: 'Alice',}")
68
- '{"name": "Alice"}'
68
+ '{"name":"Alice"}'
69
69
  """
70
70
  from .client import PAWClient
71
71
  from ._output import status
@@ -74,7 +74,13 @@ def compile(
74
74
  result = client.compile(spec, compiler=compiler, name=name, tags=tags, public=public, slug=slug)
75
75
  label = f"{result.id}"
76
76
  if result.slug:
77
- label += f" ({result.slug})"
77
+ ver_str = f" v{result.version}" if result.version and result.version > 1 else ""
78
+ action_str = ""
79
+ if result.version_action == "no_change":
80
+ action_str = " (no changes)"
81
+ elif result.version_action == "promoted":
82
+ action_str = " set as main"
83
+ label = f"{result.slug}{ver_str}{action_str}"
78
84
  status(f"Compiled: {label}")
79
85
  return result
80
86
 
@@ -84,6 +90,7 @@ def function(
84
90
  n_ctx: int = 2048,
85
91
  n_gpu_layers: int | None = None,
86
92
  verbose: bool = False,
93
+ offline: bool = False,
87
94
  ):
88
95
  """Load a compiled program for local inference via llama.cpp.
89
96
 
@@ -91,11 +98,15 @@ def function(
91
98
  Subsequent calls use the local cache.
92
99
 
93
100
  Args:
94
- program_id: Program ID (str), slug, or a ``Program`` object from compile().
101
+ program_id: Program ID (str), slug (``da03/my-program``), pinned version
102
+ (``da03/my-program@v3``), or a ``Program`` object from compile().
95
103
  n_ctx: Context window size for llama.cpp.
96
- n_gpu_layers: GPU layers (-1 = all, 0 = CPU only). Defaults to CPU.
97
- Set ``PAW_GPU_LAYERS`` env var or pass explicitly for GPU acceleration.
104
+ n_gpu_layers: GPU layers (-1 = all GPU, 0 = CPU only). Defaults to -1
105
+ (auto-uses Metal/CUDA if available, safe fallback to CPU).
106
+ Set ``PAW_GPU_LAYERS=0`` env var to force CPU-only.
98
107
  verbose: Print llama.cpp debug output.
108
+ offline: Skip server check for slug resolution and use local cache only.
109
+ Also set via ``PAW_OFFLINE=1`` env var.
99
110
 
100
111
  Returns:
101
112
  A callable ``PawFunction`` that takes an input string and returns output.
@@ -105,8 +116,7 @@ def function(
105
116
  >>> fn("Urgent: the server is down!")
106
117
  'immediate'
107
118
 
108
- >>> program = paw.compile("Classify sentiment")
109
- >>> fn = paw.function(program) # accepts Program object directly
119
+ >>> fn = paw.function("da03/my-program@v2") # pinned version
110
120
  """
111
121
  if hasattr(program_id, 'id'):
112
122
  program_id = program_id.slug or program_id.id
@@ -116,21 +126,48 @@ def function(
116
126
  from .runtime_llamacpp import PawFunction
117
127
 
118
128
  if n_gpu_layers is None:
119
- n_gpu_layers = int(os.environ.get("PAW_GPU_LAYERS", "0"))
129
+ n_gpu_layers = int(os.environ.get("PAW_GPU_LAYERS", "-1"))
120
130
 
121
131
  from ._output import status
122
132
 
123
133
  resolved_id = program_id
124
134
  if not re.fullmatch(r"[a-f0-9]{16,64}", program_id):
125
- cached = get_cached_slug(program_id)
126
- if cached:
127
- resolved_id = cached
135
+ is_pinned = bool(re.search(r"@v\d+$", program_id))
136
+ use_offline = offline or os.environ.get("PAW_OFFLINE", "").strip() in ("1", "true", "yes")
137
+
138
+ if is_pinned:
139
+ cached = get_cached_slug(program_id)
140
+ if cached:
141
+ resolved_id = cached
142
+ else:
143
+ status(f"Resolving {program_id}...")
144
+ from .client import PAWClient
145
+ client = PAWClient(api_url=get_api_url(), api_key=get_api_key())
146
+ resolved_id = client.resolve_slug(program_id)
147
+ save_slug_mapping(program_id, resolved_id)
148
+ elif use_offline:
149
+ cached = get_cached_slug(program_id)
150
+ if cached:
151
+ resolved_id = cached
152
+ else:
153
+ raise RuntimeError(
154
+ f"No cached version of '{program_id}'. Cannot resolve offline. "
155
+ "Run once with internet to populate the cache."
156
+ )
128
157
  else:
129
- status(f"Resolving {program_id}...")
130
158
  from .client import PAWClient
131
159
  client = PAWClient(api_url=get_api_url(), api_key=get_api_key())
132
- resolved_id = client.resolve_slug(program_id)
133
- save_slug_mapping(program_id, resolved_id)
160
+ try:
161
+ status(f"Resolving {program_id}...")
162
+ resolved_id = client.resolve_slug(program_id)
163
+ save_slug_mapping(program_id, resolved_id)
164
+ except Exception:
165
+ cached = get_cached_slug(program_id)
166
+ if cached:
167
+ status(f"Warning: could not reach server, using cached version of {program_id}")
168
+ resolved_id = cached
169
+ else:
170
+ raise
134
171
 
135
172
  if not is_program_cached(resolved_id):
136
173
  from .client import PAWClient
@@ -221,6 +258,25 @@ def compile_and_load(
221
258
  return function(program, n_ctx=n_ctx, n_gpu_layers=n_gpu_layers, verbose=verbose)
222
259
 
223
260
 
261
+ def list_versions(slug: str) -> dict:
262
+ """List all versions of a named program (slug).
263
+
264
+ Args:
265
+ slug: Slug in ``username/slug-name`` or bare ``slug-name`` format.
266
+
267
+ Returns:
268
+ Dict with ``slug``, ``main_version``, and ``versions`` list.
269
+
270
+ Example:
271
+ >>> versions = paw.list_versions("da03/bibtex-normalizer")
272
+ >>> for v in versions["versions"]:
273
+ ... print(f"v{v['version']}: {v['program_id'][:12]} {'(main)' if v['is_main'] else ''}")
274
+ """
275
+ from .client import PAWClient
276
+ client = PAWClient(api_url=get_api_url(), api_key=get_api_key())
277
+ return client.list_slug_versions(slug)
278
+
279
+
224
280
  def list_programs(sort: str = "recent", per_page: int = 20, page: int = 1) -> dict:
225
281
  """List your compiled programs. Requires authentication (PAW_API_KEY).
226
282
 
@@ -242,6 +298,7 @@ __all__ = [
242
298
  "compile_and_load",
243
299
  "function",
244
300
  "list_programs",
301
+ "list_versions",
245
302
  "login",
246
303
  "get_api_url",
247
304
  "get_api_key",
@@ -27,6 +27,8 @@ class Program:
27
27
  compiler_snapshot: Optional[str] = None
28
28
  timings: Optional[dict] = None
29
29
  error: Optional[str] = None
30
+ version: Optional[int] = None
31
+ version_action: Optional[str] = None
30
32
 
31
33
 
32
34
  class PAWClient:
@@ -92,6 +94,8 @@ class PAWClient:
92
94
  compiler_snapshot=data.get("compiler_snapshot"),
93
95
  timings=data.get("timings"),
94
96
  error=data.get("error"),
97
+ version=data.get("version"),
98
+ version_action=data.get("version_action"),
95
99
  )
96
100
 
97
101
  def resolve_slug(self, slug: str) -> str:
@@ -182,6 +186,16 @@ class PAWClient:
182
186
  resp.raise_for_status()
183
187
  return resp.json()
184
188
 
189
+ def list_slug_versions(self, slug: str) -> dict:
190
+ """List all versions of a slug. Slug format: 'username/slug-name' or bare 'slug-name'."""
191
+ resp = httpx.get(
192
+ f"{self._api_url}/api/v1/programs/{slug}/versions",
193
+ headers=self._headers(),
194
+ timeout=10.0,
195
+ )
196
+ resp.raise_for_status()
197
+ return resp.json()
198
+
185
199
  def list_programs(self, sort: str = "recent", per_page: int = 20, page: int = 1) -> dict:
186
200
  """List programs for the authenticated user."""
187
201
  resp = httpx.get(
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "programasweights"
7
- version = "0.2.8"
7
+ version = "0.3.1"
8
8
  description = "Compile natural language specifications into neural programs that run locally via llama.cpp."
9
9
  readme = "PYPI_README.md"
10
10
  requires-python = ">=3.9"