programasweights 0.1.0.dev6__py3-none-any.whl → 0.1.0.dev8__py3-none-any.whl

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.
@@ -1,25 +1,28 @@
1
1
  """
2
- ProgramAsWeights (PAW): Compile natural language specs into neural programs.
2
+ ProgramAsWeights (PAW): Compile natural language specs into tiny neural
3
+ functions that run locally.
3
4
 
4
5
  Quick start:
5
6
  import programasweights as paw
6
7
 
7
- # Compile on the server
8
- program = paw.compile("Classify sentiment as positive or negative")
8
+ # Use a pre-compiled function (downloads once, runs locally forever)
9
+ fn = paw.function("email-triage")
10
+ fn("Urgent: server is down!") # "immediate"
9
11
 
10
- # Run locally via llama.cpp (downloads base model on first use)
12
+ # Compile your own from a description
13
+ program = paw.compile("Fix malformed JSON: repair missing quotes and trailing commas")
11
14
  fn = paw.function(program.id)
12
- fn("I love this!") # -> "positive"
15
+ fn("{name: 'Alice',}") # '{"name": "Alice"}'
13
16
 
14
17
  API reference:
15
18
  paw.compile(spec) Compile a spec on the server, returns Program
16
19
  paw.function(program_id) Load a compiled program for local inference
17
- paw.login(email) Authenticate and store API key
20
+ paw.login() Save API key for higher rate limits
18
21
  paw.api_url Server URL (default: https://programasweights.com)
19
22
  paw.api_key API key (set via login() or PAW_API_KEY env var)
20
23
  """
21
24
 
22
- __version__ = "0.1.0.dev6"
25
+ __version__ = "0.1.0.dev8"
23
26
 
24
27
  from .config import get_api_url, get_api_key, set_api_key
25
28
 
@@ -34,41 +37,44 @@ def compile(
34
37
  name: str | None = None,
35
38
  tags: list[str] | None = None,
36
39
  public: bool = True,
40
+ slug: str | None = None,
37
41
  ):
38
42
  """Compile a natural language specification into a neural program.
39
43
 
40
44
  The compilation runs on the PAW server. The resulting program can be
41
- downloaded and run locally via ``paw.function(program.id)``.
45
+ downloaded and run locally via ``paw.function(program.id)`` or
46
+ ``paw.function(program.slug)`` if a slug was provided.
42
47
 
43
48
  Args:
44
49
  spec: Full specification text. Include examples in the text if desired.
45
50
  compiler: Compiler model (alias or snapshot name).
46
- name: Human-readable program name for the hub.
51
+ name: Human-readable program name (display title for the hub).
47
52
  tags: Tags for hub discovery.
48
53
  public: Whether to list on the public hub.
54
+ slug: URL-safe handle (e.g. 'message-classifier'). Creates a
55
+ ``username/slug`` alias. Requires authentication.
49
56
 
50
57
  Returns:
51
- A ``Program`` object with ``id``, ``status``, and ``timings``.
58
+ A ``Program`` object with ``id``, ``slug``, ``status``, and ``timings``.
52
59
 
53
60
  Example:
54
61
  >>> program = paw.compile(
55
- ... "Classify sentiment as positive or negative.\\n"
56
- ... "Examples:\\n"
57
- ... "Input: I love it\\nOutput: positive\\n"
58
- ... "Input: I hate it\\nOutput: negative"
62
+ ... "Fix malformed JSON: repair missing quotes and trailing commas",
63
+ ... slug="json-fixer"
59
64
  ... )
60
- >>> print(program.id)
61
- '4a533a4fb0a10f219384'
65
+ >>> fn = paw.function(program.slug) # or paw.function(program.id)
66
+ >>> fn("{name: 'Alice',}")
67
+ '{"name": "Alice"}'
62
68
  """
63
69
  from .client import PAWClient
64
70
  client = PAWClient(api_url=api_url, api_key=api_key)
65
- return client.compile(spec, compiler=compiler, name=name, tags=tags, public=public)
71
+ return client.compile(spec, compiler=compiler, name=name, tags=tags, public=public, slug=slug)
66
72
 
67
73
 
68
74
  def function(
69
75
  program_id: str,
70
76
  n_ctx: int = 2048,
71
- n_gpu_layers: int = -1,
77
+ n_gpu_layers: int | None = None,
72
78
  verbose: bool = False,
73
79
  ):
74
80
  """Load a compiled program for local inference via llama.cpp.
@@ -79,26 +85,36 @@ def function(
79
85
  Args:
80
86
  program_id: The program ID from ``paw.compile()``.
81
87
  n_ctx: Context window size for llama.cpp.
82
- n_gpu_layers: GPU layers (-1 = all, 0 = CPU only).
88
+ n_gpu_layers: GPU layers (-1 = all, 0 = CPU only). Defaults to CPU.
89
+ Set ``PAW_GPU_LAYERS`` env var or pass explicitly for GPU acceleration.
83
90
  verbose: Print llama.cpp debug output.
84
91
 
85
92
  Returns:
86
93
  A callable ``PawFunction`` that takes an input string and returns output.
87
94
 
88
95
  Example:
89
- >>> fn = paw.function("4a533a4fb0a10f219384")
90
- >>> fn("I love this product!")
91
- 'positive'
96
+ >>> fn = paw.function("email-triage")
97
+ >>> fn("Urgent: the server is down!")
98
+ 'immediate'
92
99
  """
100
+ import os
93
101
  import re
94
- from .cache import is_program_cached, get_program_dir
102
+ from .cache import is_program_cached, get_program_dir, get_cached_slug, save_slug_mapping
95
103
  from .runtime_llamacpp import PawFunction
96
104
 
105
+ if n_gpu_layers is None:
106
+ n_gpu_layers = int(os.environ.get("PAW_GPU_LAYERS", "0"))
107
+
97
108
  resolved_id = program_id
98
109
  if not re.fullmatch(r"[a-f0-9]{16,64}", program_id):
99
- from .client import PAWClient
100
- client = PAWClient(api_url=api_url, api_key=api_key)
101
- resolved_id = client.resolve_slug(program_id)
110
+ cached = get_cached_slug(program_id)
111
+ if cached:
112
+ resolved_id = cached
113
+ else:
114
+ from .client import PAWClient
115
+ client = PAWClient(api_url=api_url, api_key=api_key)
116
+ resolved_id = client.resolve_slug(program_id)
117
+ save_slug_mapping(program_id, resolved_id)
102
118
 
103
119
  if not is_program_cached(resolved_id):
104
120
  from .client import PAWClient
@@ -111,58 +127,52 @@ def function(
111
127
  )
112
128
 
113
129
 
114
- def login(email: str | None = None):
115
- """Authenticate with the PAW server and store the API key locally.
130
+ def login(key: str | None = None):
131
+ """Store an API key for authenticating with the PAW server.
116
132
 
117
- If email is provided, sends a verification code to that email.
118
- Prompts for the 6-digit code, then stores the API key in
119
- ``~/.config/programasweights/config.json``.
133
+ If no key is provided, opens the Settings page in a browser
134
+ and prompts interactively for the key.
135
+
136
+ Generate your API key at https://programasweights.com/settings
120
137
 
121
138
  Args:
122
- email: Email address. If None, prompts interactively.
139
+ key: API key string (``paw_sk_...``). If None, prompts interactively.
123
140
 
124
141
  Example:
125
- >>> paw.login("user@example.com")
126
- Verification code sent to user@example.com
127
- Enter code: 123456
128
- Authenticated! API key stored.
129
- """
130
- import httpx
142
+ >>> paw.login()
143
+ Generate an API key at https://programasweights.com/settings
144
+ Paste your API key: ********
145
+ API key saved.
131
146
 
132
- if email is None:
133
- email = input("Email: ").strip()
134
-
135
- url = api_url.rstrip("/")
147
+ >>> paw.login("paw_sk_abc123...")
148
+ API key saved.
149
+ """
150
+ if key is None:
151
+ settings_url = api_url.rstrip("/") + "/settings"
152
+ print(f"Generate an API key at {settings_url}")
153
+ try:
154
+ import webbrowser
155
+ webbrowser.open(settings_url)
156
+ except Exception:
157
+ pass
136
158
 
137
- # Request verification code
138
- resp = httpx.post(
139
- f"{url}/api/v1/auth/email",
140
- json={"email": email},
141
- timeout=10.0,
142
- )
143
- resp.raise_for_status()
144
- print(f"Verification code sent to {email}")
159
+ import getpass
160
+ key = getpass.getpass("Paste your API key: ").strip()
145
161
 
146
- # Prompt for code
147
- code = input("Enter code: ").strip()
162
+ if not key:
163
+ print("No key provided. Aborted.")
164
+ return
148
165
 
149
- # Verify and get API key
150
- resp = httpx.post(
151
- f"{url}/api/v1/auth/verify",
152
- json={"email": email, "code": code},
153
- timeout=10.0,
154
- )
155
- resp.raise_for_status()
156
- data = resp.json()
166
+ if not key.startswith("paw_sk_"):
167
+ print("Warning: key doesn't start with 'paw_sk_'. Saving anyway.")
157
168
 
158
- key = data.get("api_key", "")
159
169
  set_api_key(key)
160
170
 
161
171
  global api_key
162
172
  api_key = key
163
173
 
164
- print(f"Authenticated! API key stored in ~/.config/programasweights/config.json")
165
- print(f"Tier: {data.get('tier', 'unknown')}")
174
+ print("API key saved to ~/.config/programasweights/config.json")
175
+ print("You can also set the PAW_API_KEY environment variable.")
166
176
 
167
177
 
168
178
  __all__ = [
programasweights/cache.py CHANGED
@@ -10,10 +10,12 @@ Cache structure:
10
10
  adapter.gguf # ~23 MB, Q4_0 LoRA
11
11
  prompt_template.txt
12
12
  meta.json
13
+ slug_cache.json # slug -> program_id mapping
13
14
  """
14
15
 
15
16
  from __future__ import annotations
16
17
 
18
+ import json
17
19
  import os
18
20
  from pathlib import Path
19
21
 
@@ -22,11 +24,13 @@ import httpx
22
24
  from . import config
23
25
 
24
26
  BASE_MODEL_URLS = {
25
- "qwen3-0.6b-q6_k": "https://huggingface.co/yuntian-deng/Qwen3-0.6B-GGUF-Q6_K/resolve/main/qwen3-0.6b-q6_k.gguf",
27
+ "qwen3-0.6b-q6_k": "https://huggingface.co/programasweights/Qwen3-0.6B-GGUF-Q6_K/resolve/main/qwen3-0.6b-q6_k.gguf",
28
+ "gpt2-q6_k": "https://huggingface.co/programasweights/GPT2-GGUF-Q6_K/resolve/main/gpt2-q6_k.gguf",
26
29
  }
27
30
 
28
31
  INTERPRETER_TO_GGUF = {
29
32
  "Qwen/Qwen3-0.6B": "qwen3-0.6b-q6_k",
33
+ "gpt2": "gpt2-q6_k",
30
34
  }
31
35
 
32
36
 
@@ -78,3 +82,36 @@ def _download_file(url: str, dest: Path):
78
82
  mb = downloaded / 1024 / 1024
79
83
  print(f"\r {mb:.1f} MB ({pct:.0f}%)", end="", flush=True)
80
84
  print()
85
+
86
+
87
+ def _slug_cache_path() -> Path:
88
+ return config.get_cache_dir() / "slug_cache.json"
89
+
90
+
91
+ def get_cached_slug(slug: str) -> str | None:
92
+ """Look up a slug in the local cache. Returns program_id or None."""
93
+ path = _slug_cache_path()
94
+ if not path.exists():
95
+ return None
96
+ try:
97
+ data = json.loads(path.read_text())
98
+ program_id = data.get(slug)
99
+ if program_id and is_program_cached(program_id):
100
+ return program_id
101
+ except (json.JSONDecodeError, OSError):
102
+ pass
103
+ return None
104
+
105
+
106
+ def save_slug_mapping(slug: str, program_id: str) -> None:
107
+ """Save a slug -> program_id mapping to the local cache."""
108
+ path = _slug_cache_path()
109
+ data: dict = {}
110
+ if path.exists():
111
+ try:
112
+ data = json.loads(path.read_text())
113
+ except (json.JSONDecodeError, OSError):
114
+ pass
115
+ data[slug] = program_id
116
+ path.parent.mkdir(parents=True, exist_ok=True)
117
+ path.write_text(json.dumps(data))
programasweights/cli.py CHANGED
@@ -7,9 +7,12 @@ Usage:
7
7
  paw run <program_id> <input> Run a program locally
8
8
  paw login Authenticate with email
9
9
  paw info <program_id> Show program metadata
10
+
11
+ All commands support --json for structured output (agent-friendly).
10
12
  """
11
13
 
12
14
  import argparse
15
+ import json
13
16
  import sys
14
17
 
15
18
 
@@ -20,23 +23,38 @@ def cmd_compile(args):
20
23
  if args.api_key:
21
24
  paw.api_key = args.api_key
22
25
 
23
- print(f"Compiling: {args.spec[:80]}...")
24
- program = paw.compile(args.spec, compiler=args.compiler)
26
+ if not args.json:
27
+ print(f"Compiling: {args.spec[:80]}...")
28
+
29
+ program = paw.compile(args.spec, compiler=args.compiler, slug=getattr(args, 'slug', None))
30
+
31
+ if args.json:
32
+ print(json.dumps({
33
+ "program_id": program.id,
34
+ "slug": program.slug,
35
+ "status": program.status,
36
+ "error": program.error,
37
+ "timings": program.timings,
38
+ }))
39
+ return 1 if program.error else 0
25
40
 
26
41
  if program.error:
27
42
  print(f"Error: {program.error}")
28
43
  return 1
29
44
 
30
45
  print(f"Program ID: {program.id}")
46
+ if program.slug:
47
+ print(f"Slug: {program.slug}")
31
48
  print(f"Status: {program.status}")
32
49
  if program.timings:
33
50
  total = program.timings.get("total_ms", 0)
34
51
  print(f"Total time: {total:.0f}ms")
52
+ ref = program.slug or program.id
35
53
  print(f"\nTo run locally:")
36
- print(f" paw run {program.id} \"your input here\"")
54
+ print(f" paw run --program \"{ref}\" --input \"your input here\"")
37
55
  print(f"\nOr in Python:")
38
56
  print(f" import programasweights as paw")
39
- print(f" fn = paw.function(\"{program.id}\")")
57
+ print(f" fn = paw.function(\"{ref}\")")
40
58
  print(f" fn(\"your input here\")")
41
59
  return 0
42
60
 
@@ -46,9 +64,15 @@ def cmd_run(args):
46
64
  if args.api_url:
47
65
  paw.api_url = args.api_url
48
66
 
49
- fn = paw.function(args.program_id, verbose=args.verbose)
67
+ fn = paw.function(
68
+ args.program, verbose=args.verbose,
69
+ )
50
70
  result = fn(args.input, max_tokens=args.max_tokens, temperature=args.temperature)
51
- print(result)
71
+
72
+ if args.json:
73
+ print(json.dumps({"program": args.program, "input": args.input, "output": result}))
74
+ else:
75
+ print(result)
52
76
  return 0
53
77
 
54
78
 
@@ -56,7 +80,38 @@ def cmd_login(args):
56
80
  import programasweights as paw
57
81
  if args.api_url:
58
82
  paw.api_url = args.api_url
59
- paw.login(args.email)
83
+ paw.login(args.key)
84
+ return 0
85
+
86
+
87
+ def cmd_rename(args):
88
+ import programasweights as paw
89
+ if args.api_url:
90
+ paw.api_url = args.api_url
91
+ if args.api_key:
92
+ paw.api_key = args.api_key
93
+
94
+ import httpx
95
+ from programasweights.client import PAWClient
96
+ client = PAWClient(api_url=paw.api_url, api_key=paw.api_key)
97
+
98
+ resp = httpx.patch(
99
+ f"{client._api_url}/api/v1/programs/{args.program}",
100
+ json={"slug": args.new_slug},
101
+ headers=client._headers(),
102
+ timeout=10.0,
103
+ )
104
+ resp.raise_for_status()
105
+ data = resp.json()
106
+
107
+ if args.json:
108
+ print(json.dumps(data))
109
+ else:
110
+ slug = data.get("slug")
111
+ if slug:
112
+ print(f"Renamed to: {slug}")
113
+ else:
114
+ print("Slug removed.")
60
115
  return 0
61
116
 
62
117
 
@@ -65,23 +120,32 @@ def cmd_info(args):
65
120
  if args.api_url:
66
121
  paw.api_url = args.api_url
67
122
 
68
- from programasweights.cache import is_program_cached, get_program_dir
69
- import json
123
+ from programasweights.client import PAWClient
124
+ client = PAWClient(api_url=paw.api_url, api_key=paw.api_key)
125
+
126
+ try:
127
+ meta = client.get_program_meta(args.program)
128
+ except Exception:
129
+ meta = None
70
130
 
71
- if is_program_cached(args.program_id):
72
- d = get_program_dir(args.program_id)
73
- meta_path = d / "meta.json"
74
- if meta_path.exists():
75
- meta = json.loads(meta_path.read_text())
76
- print(f"Program: {args.program_id}")
77
- print(f" Spec: {meta.get('spec', 'N/A')[:100]}")
131
+ if meta and meta.get("id"):
132
+ if args.json:
133
+ print(json.dumps(meta))
134
+ else:
135
+ print(f"Program: {meta.get('id')}")
136
+ print(f" Spec: {(meta.get('spec') or 'N/A')[:100]}")
78
137
  print(f" Interpreter: {meta.get('interpreter', 'N/A')}")
79
138
  print(f" Compiler: {meta.get('compiler_snapshot', 'N/A')}")
80
- print(f" LoRA rank: {meta.get('lora_rank', 'N/A')}")
81
- print(f" Created: {meta.get('created_at', 'N/A')}")
82
- return 0
139
+ print(f" Aliases: {', '.join(meta.get('aliases', []))}")
140
+ print(f" Downloads: {meta.get('downloads', 0)}")
141
+ if meta.get('hf_url'):
142
+ print(f" HF URL: {meta['hf_url']}")
143
+ return 0
83
144
 
84
- print(f"Program {args.program_id} not cached locally.")
145
+ if args.json:
146
+ print(json.dumps({"error": "not_found", "program": args.program}))
147
+ else:
148
+ print(f"Program {args.program} not found.")
85
149
  return 1
86
150
 
87
151
 
@@ -92,29 +156,36 @@ def main():
92
156
  )
93
157
  parser.add_argument("--api-url", default=None, help="PAW server URL")
94
158
  parser.add_argument("--api-key", default=None, help="API key")
159
+ parser.add_argument("--json", action="store_true",
160
+ help="Output structured JSON (agent-friendly)")
95
161
 
96
162
  sub = parser.add_subparsers(dest="command")
97
163
 
98
- # paw compile
99
164
  p = sub.add_parser("compile", help="Compile a spec on the server")
100
- p.add_argument("spec", help="Natural language specification")
165
+ p.add_argument("--spec", required=True, help="Natural language specification")
101
166
  p.add_argument("--compiler", default="paw-4b-qwen3-0.6b", help="Compiler model")
167
+ p.add_argument("--slug", default=None, help="URL-safe handle (e.g. 'message-classifier')")
168
+ p.add_argument("--json", action="store_true", help="JSON output")
102
169
 
103
- # paw run
104
170
  p = sub.add_parser("run", help="Run a program locally via llama.cpp")
105
- p.add_argument("program_id", help="Program ID")
106
- p.add_argument("input", help="Input text")
171
+ p.add_argument("--program", required=True, help="Program name or ID")
172
+ p.add_argument("--input", required=True, help="Input text")
107
173
  p.add_argument("--max-tokens", type=int, default=512)
108
174
  p.add_argument("--temperature", type=float, default=0.0)
109
175
  p.add_argument("--verbose", action="store_true")
176
+ p.add_argument("--json", action="store_true", help="JSON output")
177
+
178
+ p = sub.add_parser("login", help="Save API key for authentication")
179
+ p.add_argument("key", nargs="?", default=None, help="API key (paw_sk_...). Omit to open browser.")
110
180
 
111
- # paw login
112
- p = sub.add_parser("login", help="Authenticate with email")
113
- p.add_argument("email", nargs="?", default=None, help="Email address")
181
+ p = sub.add_parser("rename", help="Set or change a program's slug")
182
+ p.add_argument("program", help="Program ID or current slug")
183
+ p.add_argument("new_slug", help="New slug (e.g. 'message-classifier') or empty string to remove")
184
+ p.add_argument("--json", action="store_true", help="JSON output")
114
185
 
115
- # paw info
116
186
  p = sub.add_parser("info", help="Show program info")
117
- p.add_argument("program_id", help="Program ID")
187
+ p.add_argument("program", help="Program name or ID")
188
+ p.add_argument("--json", action="store_true", help="JSON output")
118
189
 
119
190
  args = parser.parse_args()
120
191
 
@@ -126,6 +197,7 @@ def main():
126
197
  "compile": cmd_compile,
127
198
  "run": cmd_run,
128
199
  "login": cmd_login,
200
+ "rename": cmd_rename,
129
201
  "info": cmd_info,
130
202
  }
131
203
 
@@ -23,6 +23,7 @@ class Program:
23
23
  """Result of a compilation."""
24
24
  id: str
25
25
  status: str
26
+ slug: Optional[str] = None
26
27
  compiler_snapshot: Optional[str] = None
27
28
  timings: Optional[dict] = None
28
29
  error: Optional[str] = None
@@ -48,27 +49,32 @@ class PAWClient:
48
49
  name: str | None = None,
49
50
  tags: list[str] | None = None,
50
51
  public: bool = True,
52
+ slug: str | None = None,
51
53
  ) -> Program:
52
54
  """Compile a spec into a neural program on the server.
53
55
 
54
56
  Args:
55
57
  spec: Natural language specification. Include examples in the text.
56
58
  compiler: Compiler name (alias or snapshot).
57
- name: Human-readable program name.
59
+ name: Human-readable program name (display title).
58
60
  tags: Tags for hub discovery.
59
61
  public: Whether to list on the public hub.
62
+ slug: URL-safe handle (e.g. 'message-classifier'). Creates a
63
+ ``username/slug`` alias for easy reference. Requires auth.
60
64
 
61
65
  Returns:
62
- Program with id, status, and timings.
66
+ Program with id, slug, status, and timings.
63
67
 
64
68
  Raises:
65
69
  httpx.HTTPStatusError: On API errors (422 for validation, 429 for rate limit).
66
70
  """
67
- body = {"spec": spec, "compiler": compiler, "public": public}
71
+ body: dict = {"spec": spec, "compiler": compiler, "public": public}
68
72
  if name:
69
73
  body["name"] = name
70
74
  if tags:
71
75
  body["tags"] = tags
76
+ if slug:
77
+ body["slug"] = slug
72
78
 
73
79
  resp = httpx.post(
74
80
  f"{self._api_url}/api/v1/compile",
@@ -82,6 +88,7 @@ class PAWClient:
82
88
  return Program(
83
89
  id=data.get("program_id", ""),
84
90
  status=data.get("status", "unknown"),
91
+ slug=data.get("slug"),
85
92
  compiler_snapshot=data.get("compiler_snapshot"),
86
93
  timings=data.get("timings"),
87
94
  error=data.get("error"),
@@ -82,7 +82,7 @@ def download_onnx_models(model_name: str, download_image_encoder: bool = False)
82
82
  Downloads only required files (lazy loading for optional components).
83
83
 
84
84
  Args:
85
- model_name: HuggingFace repo ID (e.g., "yuntian-deng/paw-interpreter-onnx")
85
+ model_name: HuggingFace repo ID (e.g., "programasweights/paw-interpreter-onnx")
86
86
  OR local path to ONNX models directory
87
87
  download_image_encoder: If True, also download image encoder (default: False, lazy)
88
88
 
@@ -3,13 +3,15 @@ llama.cpp runtime for local inference with LoRA adapters.
3
3
 
4
4
  Loads a base GGUF model (Q6_K) and applies a Q4_0 LoRA adapter per-program.
5
5
  Uses the pre-rendered prompt template from the .paw bundle.
6
+
7
+ Prefix KV cache is saved to disk after the first call and reloaded on
8
+ subsequent runs, eliminating the ~2-3s cold-start prefix evaluation.
6
9
  """
7
10
 
8
11
  from __future__ import annotations
9
12
 
10
13
  import json
11
14
  from pathlib import Path
12
- from typing import Callable
13
15
 
14
16
  import llama_cpp
15
17
  from llama_cpp import Llama
@@ -29,13 +31,13 @@ class PawFunction:
29
31
  self,
30
32
  program_dir: str | Path,
31
33
  n_ctx: int = 2048,
32
- n_gpu_layers: int = -1,
34
+ n_gpu_layers: int = 0,
33
35
  verbose: bool = False,
34
36
  ):
35
37
  program_dir = Path(program_dir)
36
38
  self._program_dir = program_dir
39
+ self._verbose = verbose
37
40
 
38
- # Load metadata
39
41
  meta_path = program_dir / "meta.json"
40
42
  if meta_path.exists():
41
43
  with open(meta_path) as f:
@@ -43,17 +45,14 @@ class PawFunction:
43
45
  else:
44
46
  self._meta = {}
45
47
 
46
- # Load prompt template
47
48
  template_path = program_dir / "prompt_template.txt"
48
49
  if not template_path.exists():
49
50
  raise FileNotFoundError(f"No prompt_template.txt in {program_dir}")
50
51
  self._template = template_path.read_text()
51
52
 
52
- # Get base model GGUF
53
53
  interpreter = self._meta.get("interpreter", "Qwen/Qwen3-0.6B")
54
54
  base_model_path = cache.get_base_model_path(interpreter)
55
55
 
56
- # Load base model
57
56
  self._llm = Llama(
58
57
  model_path=str(base_model_path),
59
58
  n_ctx=n_ctx,
@@ -61,7 +60,6 @@ class PawFunction:
61
60
  verbose=verbose,
62
61
  )
63
62
 
64
- # Load LoRA adapter
65
63
  adapter_path = program_dir / "adapter.gguf"
66
64
  if adapter_path.exists():
67
65
  self._adapter = llama_cpp.llama_adapter_lora_init(
@@ -74,6 +72,69 @@ class PawFunction:
74
72
  else:
75
73
  self._adapter = None
76
74
 
75
+ placeholder = "{INPUT_PLACEHOLDER}"
76
+ self._use_special = interpreter not in ("gpt2",)
77
+
78
+ if placeholder in self._template:
79
+ prefix_text = self._template.split(placeholder)[0]
80
+ suffix_text = self._template.split(placeholder)[1]
81
+ else:
82
+ prefix_text = self._template
83
+ suffix_text = ""
84
+
85
+ self._prefix_tokens = self._llm.tokenize(
86
+ prefix_text.encode("utf-8"),
87
+ add_bos=not self._use_special,
88
+ special=self._use_special,
89
+ )
90
+ self._suffix_text = suffix_text
91
+ self._n_prefix = len(self._prefix_tokens)
92
+
93
+ self._load_or_eval_prefix()
94
+
95
+ def _load_or_eval_prefix(self):
96
+ """Load prefix KV state from disk cache, or evaluate and save it."""
97
+ cache_path = self._program_dir / "prefix_kv_cache.bin"
98
+
99
+ if cache_path.exists():
100
+ try:
101
+ import ctypes
102
+ token_array = (llama_cpp.llama_token * self._n_prefix)(*self._prefix_tokens)
103
+ n_token_count = ctypes.c_size_t(0)
104
+ n_loaded = llama_cpp.llama_state_seq_load_file(
105
+ self._llm.ctx,
106
+ str(cache_path).encode("utf-8"),
107
+ 0,
108
+ token_array,
109
+ self._n_prefix,
110
+ ctypes.byref(n_token_count),
111
+ )
112
+ if n_loaded > 0:
113
+ self._llm.n_tokens = self._n_prefix
114
+ self._llm.input_ids[:self._n_prefix] = self._prefix_tokens
115
+ if self._verbose:
116
+ print(f"Loaded prefix KV cache ({self._n_prefix} tokens) from disk")
117
+ return
118
+ except Exception:
119
+ pass
120
+
121
+ self._llm.eval(self._prefix_tokens)
122
+
123
+ try:
124
+ token_array = (llama_cpp.llama_token * self._n_prefix)(*self._prefix_tokens)
125
+ result = llama_cpp.llama_state_seq_save_file(
126
+ self._llm.ctx,
127
+ str(cache_path).encode("utf-8"),
128
+ 0,
129
+ token_array,
130
+ self._n_prefix,
131
+ )
132
+ if result and self._verbose:
133
+ size_mb = cache_path.stat().st_size / (1024 * 1024)
134
+ print(f"Saved prefix KV cache ({self._n_prefix} tokens, {size_mb:.1f} MB)")
135
+ except Exception:
136
+ pass
137
+
77
138
  def __call__(
78
139
  self,
79
140
  input_text: str,
@@ -90,20 +151,32 @@ class PawFunction:
90
151
  Returns:
91
152
  The program's output as a string.
92
153
  """
93
- rendered = self._template.replace("{INPUT_PLACEHOLDER}", input_text)
94
-
95
- token_ids = self._llm.tokenize(
96
- rendered.encode("utf-8"), add_bos=False, special=True,
154
+ # Reset to prefix state: clear everything after the prefix
155
+ self._llm.n_tokens = self._n_prefix
156
+
157
+ input_with_suffix = input_text + self._suffix_text
158
+ input_tokens = self._llm.tokenize(
159
+ input_with_suffix.encode("utf-8"),
160
+ add_bos=False,
161
+ special=self._use_special,
97
162
  )
98
163
 
99
- out = self._llm.create_completion(
100
- prompt=token_ids,
101
- max_tokens=max_tokens,
102
- temperature=temperature if temperature > 0 else 0,
103
- echo=False,
104
- )
164
+ self._llm.eval(input_tokens)
165
+
166
+ output_tokens = []
167
+ for _ in range(max_tokens):
168
+ token = self._llm.sample(
169
+ temp=temperature if temperature > 0 else 0,
170
+ )
171
+
172
+ if token == self._llm.token_eos():
173
+ break
174
+
175
+ output_tokens.append(token)
176
+ self._llm.eval([token])
105
177
 
106
- return out["choices"][0]["text"].strip()
178
+ output_bytes = self._llm.detokenize(output_tokens)
179
+ return output_bytes.decode("utf-8", errors="replace").strip()
107
180
 
108
181
  def __del__(self):
109
182
  if hasattr(self, "_adapter") and self._adapter:
@@ -0,0 +1,142 @@
1
+ Metadata-Version: 2.4
2
+ Name: programasweights
3
+ Version: 0.1.0.dev8
4
+ Summary: Compile natural language specifications into neural programs that run locally via llama.cpp.
5
+ Project-URL: Homepage, https://programasweights.com
6
+ Project-URL: Repository, https://github.com/programasweights/programasweights-python
7
+ Project-URL: Documentation, https://programasweights.readthedocs.io
8
+ Project-URL: Bug Tracker, https://github.com/programasweights/programasweights-python/issues
9
+ Author-email: ProgramAsWeights <support@programasweights.com>
10
+ License: MIT
11
+ Keywords: inference,llama-cpp,lora,neural-programs,nlp
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Requires-Python: >=3.9
24
+ Requires-Dist: httpx<1.0,>=0.27.0
25
+ Requires-Dist: llama-cpp-python<1.0,>=0.3.0
26
+ Provides-Extra: test
27
+ Requires-Dist: pytest; extra == 'test'
28
+ Description-Content-Type: text/markdown
29
+
30
+ # ProgramAsWeights
31
+
32
+ **Compile natural language specs into tiny neural functions that run locally.**
33
+
34
+ Define what a function should do in plain English. PAW compiles it into a small neural program that runs on your machine — no API keys at runtime, no internet needed after setup, fully deterministic.
35
+
36
+ ## Install
37
+
38
+ ```bash
39
+ pip install programasweights
40
+ ```
41
+
42
+ ## Quick Start
43
+
44
+ ```python
45
+ import programasweights as paw
46
+
47
+ # Use a pre-compiled function (downloads once, runs locally forever)
48
+ fn = paw.function("email-triage")
49
+ fn("Urgent: the server is down!") # "immediate"
50
+ fn("Newsletter: spring picnic") # "wait"
51
+
52
+ # Compile your own from a description
53
+ program = paw.compile(
54
+ "Fix malformed JSON: repair missing quotes and trailing commas",
55
+ compiler="paw-4b-qwen3-0.6b" # or "paw-4b-gpt2" for smaller/faster
56
+ )
57
+ fn = paw.function(program.id)
58
+ fn("{name: 'Alice',}") # '{"name": "Alice"}'
59
+ ```
60
+
61
+ ## Two Compilers
62
+
63
+ | | Standard (Qwen3 0.6B) | Compact (GPT-2 124M) |
64
+ |--------------------|-------------------------|------------------------|
65
+ | Compiler name | `paw-4b-qwen3-0.6b` | `paw-4b-gpt2` |
66
+ | Accuracy | Higher | Lower |
67
+ | Base model size | 594 MB | 105 MB |
68
+ | Program size | ~22 MB | ~5 MB |
69
+ | Inference speed | ~90ms (server) | ~50ms (server) |
70
+ | Runs in browser | No | Yes |
71
+
72
+ Default is Standard (Qwen3 0.6B). Use Compact (GPT-2) when you need smaller files or browser deployment.
73
+
74
+ ## Browser SDK
75
+
76
+ Programs compiled with GPT-2 also run entirely in the browser via WebAssembly — no server needed, data never leaves the user's device.
77
+
78
+ ```bash
79
+ npm install @programasweights/web
80
+ ```
81
+
82
+ ```javascript
83
+ import paw from '@programasweights/web';
84
+
85
+ const fn = await paw.function('programasweights/email-triage');
86
+ const result = await fn('Urgent: the server is down!');
87
+ // result: "immediate"
88
+ ```
89
+
90
+ See the [browser SDK repo](https://github.com/programasweights/programasweights-js) for full documentation.
91
+
92
+ ## Use with AI Agents
93
+
94
+ PAW works with Cursor, Claude, Codex, and other AI coding assistants. Paste this into your agent's chat:
95
+
96
+ > 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.
97
+
98
+ Or save [`AGENTS.md`](https://programasweights.com/agents) to your project root — agents read it automatically.
99
+
100
+ ## When to Use PAW
101
+
102
+ - **Fuzzy search** — typo-tolerant matching, semantic search, near-duplicate detection
103
+ - **Format repair** — fix broken JSON, normalize dates, repair malformed inputs
104
+ - **Classification** — sentiment, urgency, categories defined in your own words
105
+ - **Extraction** — emails, names, dates from messy unstructured text
106
+ - **Log triage** — extract errors from verbose output, filter noise
107
+ - **Intent routing** — map user descriptions to the closest URL, menu item, or setting
108
+ - **Agent preprocessing** — parse tool calls, validate outputs, route tasks
109
+
110
+ ## Authentication
111
+
112
+ ```bash
113
+ # Option 1: environment variable (recommended)
114
+ export PAW_API_KEY=paw_sk_...
115
+
116
+ # Option 2: CLI login (opens browser to generate key)
117
+ paw login
118
+ ```
119
+
120
+ Generate API keys at [programasweights.com/settings](https://programasweights.com/settings). Authenticated users get higher rate limits.
121
+
122
+ ## CLI
123
+
124
+ ```bash
125
+ paw compile --spec "Extract error lines from logs" --json
126
+ paw run --program <program_id> --input "[ERROR] timeout" --json
127
+ paw login
128
+ ```
129
+
130
+ `--json` gives structured output for programmatic use.
131
+
132
+ ## Links
133
+
134
+ - **Website**: [programasweights.com](https://programasweights.com)
135
+ - **Documentation**: [programasweights.readthedocs.io](https://programasweights.readthedocs.io)
136
+ - **Python SDK**: [github.com/programasweights/programasweights-python](https://github.com/programasweights/programasweights-python)
137
+ - **Browser SDK**: [github.com/programasweights/programasweights-js](https://github.com/programasweights/programasweights-js)
138
+ - **Program Hub**: [programasweights.com/hub](https://programasweights.com/hub)
139
+
140
+ ## License
141
+
142
+ MIT
@@ -1,18 +1,18 @@
1
- programasweights/__init__.py,sha256=Sv7JjEKehJHLalI1Cvpx6cIoB0d74oEdZTvoiDwlLz0,5160
1
+ programasweights/__init__.py,sha256=i6zgstkPcoxr00hkH_qWfFvBtRKaxjp87AUYirZeGp0,5952
2
2
  programasweights/artifacts.py,sha256=bSRZgYadAYyuH9aIW6P3VocExfGDGAHeDuj8vod5-Bo,1968
3
- programasweights/cache.py,sha256=5eXxonCfaEX0MvvffG_takwmB0nUCXtorb8Vp_B_p_4,2524
4
- programasweights/cli.py,sha256=nO896Xfhc_nWL2YaluxprIMTmPM6gP2F5MSPBZT_kJo,4043
5
- programasweights/client.py,sha256=1IAl1YL4P2wNbcVpy2cDqL4WjCL3Z2T1orNIXDtcrrw,4067
3
+ programasweights/cache.py,sha256=69mOsx2Aqi3sQjFXjeBvhKySXDKdvKSINis2FprVDw4,3711
4
+ programasweights/cli.py,sha256=_HCBRXfn9at7a4oTvVOAi2lPMMKaDxleLngdc6WKi2c,6480
5
+ programasweights/client.py,sha256=Qk0ubKW54robuVnvEf0AyED5zX2Rw-W4HefBg_GQp8s,4391
6
6
  programasweights/config.py,sha256=zQFnVNfYR4bFx2DsZheMnPy2yd9T4-fe0u2sG1GluE8,1553
7
7
  programasweights/convert_peft_to_paw.py,sha256=yvavaAzLUqc-lFHEC_9AOhsuI-fCobu6k-GDMVQdOHk,6521
8
8
  programasweights/paw_format.py,sha256=wRXolwtnPgKoopiJC8-yMVZZtiwB-PoAqaWpDFKMoj8,10679
9
- programasweights/runtime_llamacpp.py,sha256=ErPsHMtic1t_JZkP66Zl4SXgJi7VBx0kd66s3xBjc_4,3701
9
+ programasweights/runtime_llamacpp.py,sha256=UWcmhKF9iNA2r3gDIKxP5bcdJ2WUvSNilW0_aMfvmuo,6461
10
10
  programasweights/compiler/__init__.py,sha256=_q0L02k6Cl9zyvxKQlMuW2o6w7cX6OZ_bZ9wHuZqs18,9508
11
11
  programasweights/compiler/dummy.py,sha256=PcLwijRNM4q2E9PNUcfCPf_y08QjsteVENR0TVsOIfY,1011
12
12
  programasweights/runtime/__init__.py,sha256=S4jp7-eWAcMa0X417U7P6HkTL1j3v8ffQA-nneh9wY0,549
13
13
  programasweights/runtime/interpreter.py,sha256=brYCtatSsu23lq87cuki4MViqZX6U32LBw7Z3USbQmA,19632
14
- programasweights/runtime/interpreter_onnx.py,sha256=lim1FklSyP8Htx1UYLFCajdmOy3fbNaXpey1ryMfdr4,21555
15
- programasweights-0.1.0.dev6.dist-info/METADATA,sha256=2tU55n6X6W7TZoxRLvdUv4hxbXC4PoxFjW-m_ZpiN-A,4007
16
- programasweights-0.1.0.dev6.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
17
- programasweights-0.1.0.dev6.dist-info/entry_points.txt,sha256=l4ZnfCPU0oMzhGB9T2Fv9AnDbSCMM70KUUJVtSvMJBc,50
18
- programasweights-0.1.0.dev6.dist-info/RECORD,,
14
+ programasweights/runtime/interpreter_onnx.py,sha256=kzYKmwg_h0tVhoQlixJGKoHPVhcCK22AcHG59oeYX2Q,21559
15
+ programasweights-0.1.0.dev8.dist-info/METADATA,sha256=yeRSFIIH216ZjEOEFlKTqIHN-BiyA1bFo_cXCnN3nls,5574
16
+ programasweights-0.1.0.dev8.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
17
+ programasweights-0.1.0.dev8.dist-info/entry_points.txt,sha256=l4ZnfCPU0oMzhGB9T2Fv9AnDbSCMM70KUUJVtSvMJBc,50
18
+ programasweights-0.1.0.dev8.dist-info/RECORD,,
@@ -1,127 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: programasweights
3
- Version: 0.1.0.dev6
4
- Summary: Compile natural language specifications into neural programs that run locally via llama.cpp.
5
- Author-email: ProgramAsWeights <support@programasweights.com>
6
- License: MIT
7
- Keywords: inference,llama-cpp,lora,neural-programs,nlp
8
- Classifier: Development Status :: 3 - Alpha
9
- Classifier: Intended Audience :: Developers
10
- Classifier: Intended Audience :: Science/Research
11
- Classifier: License :: OSI Approved :: MIT License
12
- Classifier: Programming Language :: Python :: 3
13
- Classifier: Programming Language :: Python :: 3.9
14
- Classifier: Programming Language :: Python :: 3.10
15
- Classifier: Programming Language :: Python :: 3.11
16
- Classifier: Programming Language :: Python :: 3.12
17
- Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
18
- Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
- Requires-Python: >=3.9
20
- Requires-Dist: httpx<1.0,>=0.27.0
21
- Requires-Dist: llama-cpp-python<1.0,>=0.3.0
22
- Provides-Extra: test
23
- Requires-Dist: pytest; extra == 'test'
24
- Description-Content-Type: text/markdown
25
-
26
- # ProgramAsWeights
27
-
28
- **Compile natural language specifications into neural programs (.paw files) that run locally.**
29
-
30
- Programs are stored as weight blobs (KV cache prefix + optional LoRA adapters) interpreted by a small fixed model. No API calls needed at runtime — fully deterministic, local execution.
31
-
32
- ## Installation
33
-
34
- ```bash
35
- pip install programasweights
36
- ```
37
-
38
- ## Quick Start
39
-
40
- ### Run a Program
41
-
42
- ```python
43
- import programasweights as paw
44
-
45
- # Load and run a compiled program
46
- fn = paw.function("program_id_or_path.paw")
47
- result = fn("Contact alice@company.com or bob@example.org")
48
- print(result) # ["alice@company.com", "bob@example.org"]
49
- ```
50
-
51
- ### Compile a Program
52
-
53
- ```python
54
- import programasweights as paw
55
-
56
- # Compile from natural language specification
57
- paw.compile(
58
- "output.paw",
59
- spec="Extract all email addresses from text and return as JSON list",
60
- checkpoint_dir="path/to/trained/compiler",
61
- )
62
- ```
63
-
64
- ## LoRA Support (PEFT Compatible)
65
-
66
- Already using PEFT for LoRA training? Convert to .paw in one line:
67
-
68
- ```python
69
- import programasweights as paw
70
-
71
- # Standard PEFT workflow:
72
- # model = get_peft_model(base_model, LoraConfig(r=16, target_modules=["q_proj", "v_proj"]))
73
- # trainer.train()
74
- # model.save_pretrained("my_adapter/")
75
-
76
- # Convert to .paw:
77
- paw.from_peft(
78
- "my_adapter/", # Your PEFT checkpoint
79
- "sentiment.paw", # Output .paw file
80
- spec="Classify sentiment as positive or negative",
81
- tags=["sentiment", "classification"],
82
- examples=[
83
- {"input": "Great movie!", "output": "positive"},
84
- {"input": "Terrible film.", "output": "negative"},
85
- ],
86
- )
87
-
88
- # Use it:
89
- fn = paw.function("sentiment.paw")
90
- print(fn("This is amazing!")) # → "positive"
91
- ```
92
-
93
- Load LoRA from a .paw file:
94
-
95
- ```python
96
- lora_weights, lora_config = paw.load_paw_lora("sentiment.paw")
97
- print(lora_config) # {"rank": 16, "alpha": 32, ...}
98
- ```
99
-
100
- Or use `save_lora_to_paw()` directly if you have raw tensors instead of a PEFT checkpoint.
101
-
102
- ## .paw File Format v2
103
-
104
- A `.paw` file is a self-contained neural program that includes:
105
-
106
- | Component | Description | Required |
107
- |-----------|-------------|----------|
108
- | KV cache prefix | Continuous program (prefix weights) | Optional |
109
- | Pseudo-program | Discrete text instructions | Optional |
110
- | LoRA adapter | Fine-tuned adapter weights | Optional |
111
- | Generation config | Temperature, top_p, max_tokens | Optional |
112
- | Metadata | Interpreter model, spec, author, tags | Required |
113
-
114
- ## Program Hub
115
-
116
- Browse and share programs at [hub.programasweights.com](https://hub.programasweights.com)
117
-
118
- ## Links
119
-
120
- - **Website**: [programasweights.com](https://programasweights.com)
121
- - **Documentation**: [programasweights.readthedocs.io](https://programasweights.readthedocs.io)
122
- - **GitHub**: [github.com/programasweights/programasweights](https://github.com/programasweights/programasweights)
123
- - **Program Hub**: [hub.programasweights.com](https://hub.programasweights.com)
124
-
125
- ## License
126
-
127
- MIT