Skip to content

Commit e95d7c8

Browse files
feat: Add Sweep 1.5B next-edit prediction addon
- Add @stackmemoryai/sweep-addon package with Python inference - Integrate CLI commands: sweep setup, status, predict - Model downloads from HuggingFace on first use - Published to npm as @stackmemoryai/sweep-addon@0.1.0 - Add packages/ to eslint ignores
1 parent dbe9cdd commit e95d7c8

9 files changed

Lines changed: 1046 additions & 0 deletions

File tree

eslint.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export default [
4848
'templates/',
4949
'archive/',
5050
'external/',
51+
'packages/',
5152
'*.js',
5253
'*.min.js',
5354
'*.bundle.js',

packages/sweep-addon/README.md

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# @stackmemory/sweep-addon
2+
3+
Optional addon for StackMemory that provides next-edit predictions using the Sweep 1.5B model.
4+
5+
## Overview
6+
7+
Sweep 1.5B is a code completion model trained to predict the next edit you'll make based on:
8+
- Current file content
9+
- Recent diffs (what you just changed)
10+
- Context from other files in your codebase
11+
12+
## Requirements
13+
14+
- Node.js 18+
15+
- Python 3.10+
16+
- pip packages: `huggingface_hub`, `llama-cpp-python`
17+
18+
## Installation
19+
20+
### Via CLI
21+
22+
```bash
23+
stackmemory sweep setup
24+
```
25+
26+
This installs the required Python dependencies.
27+
28+
### Manual
29+
30+
```bash
31+
pip install huggingface_hub llama-cpp-python
32+
```
33+
34+
## Usage
35+
36+
### CLI
37+
38+
```bash
39+
# Check status
40+
stackmemory sweep status
41+
42+
# Predict next edit for a file
43+
stackmemory sweep predict src/app.ts
44+
45+
# Setup with model pre-download
46+
stackmemory sweep setup --download
47+
```
48+
49+
### Programmatic (TypeScript)
50+
51+
```typescript
52+
import { predict, checkStatus } from '@stackmemory/sweep-addon';
53+
54+
// Check if addon is ready
55+
const status = await checkStatus();
56+
console.log(status.installed, status.model_downloaded);
57+
58+
// Run prediction
59+
const result = await predict({
60+
file_path: 'src/app.ts',
61+
current_content: '...',
62+
context_files: {
63+
'src/utils.ts': '...'
64+
},
65+
recent_diffs: [{
66+
file_path: 'src/app.ts',
67+
original: '...',
68+
updated: '...'
69+
}]
70+
});
71+
72+
if (result.success) {
73+
console.log(result.predicted_content);
74+
}
75+
```
76+
77+
## Model
78+
79+
The Sweep 1.5B model (~1.5GB GGUF Q8 quantized) is downloaded from HuggingFace on first use:
80+
- Repo: `sweepai/sweep-next-edit-1.5B`
81+
- File: `sweep-next-edit-1.5b.q8_0.v2.gguf`
82+
- Location: `~/.stackmemory/models/sweep/`
83+
84+
## License
85+
86+
MIT

packages/sweep-addon/package.json

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
{
2+
"name": "@stackmemoryai/sweep-addon",
3+
"version": "0.1.0",
4+
"description": "Sweep 1.5B next-edit prediction addon for StackMemory",
5+
"type": "module",
6+
"main": "dist/index.js",
7+
"types": "dist/index.d.ts",
8+
"bin": {
9+
"sweep-setup": "./scripts/setup.sh"
10+
},
11+
"scripts": {
12+
"build": "tsc",
13+
"setup": "./scripts/setup.sh",
14+
"test": "node dist/test.js"
15+
},
16+
"dependencies": {},
17+
"peerDependencies": {
18+
"@stackmemoryai/stackmemory": ">=0.5.0"
19+
},
20+
"devDependencies": {
21+
"typescript": "^5.0.0"
22+
},
23+
"engines": {
24+
"node": ">=18.0.0"
25+
},
26+
"files": [
27+
"dist",
28+
"scripts",
29+
"python"
30+
],
31+
"keywords": [
32+
"stackmemory",
33+
"sweep",
34+
"next-edit",
35+
"autocomplete",
36+
"ai"
37+
],
38+
"license": "MIT",
39+
"repository": {
40+
"type": "git",
41+
"url": "https://github.com/stackmemoryai/stackmemory.git",
42+
"directory": "packages/sweep-addon"
43+
}
44+
}
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Sweep Next-Edit 1.5B prediction script for StackMemory integration.
4+
5+
This script provides next-edit predictions using the Sweep 1.5B model.
6+
It reads input from stdin (JSON) and outputs predictions to stdout.
7+
8+
Usage:
9+
echo '{"file_path": "...", "current_content": "...", ...}' | python sweep_predict.py
10+
"""
11+
import json
12+
import sys
13+
import os
14+
from pathlib import Path
15+
16+
# Model configuration
17+
MODEL_REPO = "sweepai/sweep-next-edit-1.5B"
18+
MODEL_FILENAME = "sweep-next-edit-1.5b.q8_0.v2.gguf"
19+
MODEL_DIR = Path.home() / ".stackmemory" / "models" / "sweep"
20+
21+
22+
def get_model_path() -> Path:
23+
"""Get path to the model file, downloading if necessary."""
24+
model_path = MODEL_DIR / MODEL_FILENAME
25+
26+
if model_path.exists():
27+
return model_path
28+
29+
# Download model
30+
print(json.dumps({"status": "downloading", "message": "Downloading Sweep 1.5B model..."}), file=sys.stderr)
31+
32+
try:
33+
from huggingface_hub import hf_hub_download
34+
35+
MODEL_DIR.mkdir(parents=True, exist_ok=True)
36+
37+
downloaded_path = hf_hub_download(
38+
repo_id=MODEL_REPO,
39+
filename=MODEL_FILENAME,
40+
repo_type="model",
41+
local_dir=MODEL_DIR,
42+
local_dir_use_symlinks=False
43+
)
44+
45+
print(json.dumps({"status": "downloaded", "path": str(downloaded_path)}), file=sys.stderr)
46+
return Path(downloaded_path)
47+
48+
except ImportError:
49+
print(json.dumps({
50+
"error": "huggingface_hub not installed",
51+
"message": "Run: pip install huggingface_hub llama-cpp-python"
52+
}))
53+
sys.exit(1)
54+
except Exception as e:
55+
print(json.dumps({"error": "download_failed", "message": str(e)}))
56+
sys.exit(1)
57+
58+
59+
def build_prompt(
60+
context_files: dict[str, str],
61+
recent_diffs: list[dict[str, str]],
62+
file_path: str,
63+
original_content: str,
64+
current_content: str,
65+
) -> str:
66+
"""
67+
Build a prompt following Sweep Next Edit's training format.
68+
69+
Format uses <|file_sep|> tokens to separate sections:
70+
- Context files
71+
- Recent diffs (original/updated blocks)
72+
- Original file state
73+
- Current file state
74+
- Updated file state (to be predicted)
75+
"""
76+
prompt_parts = []
77+
78+
# Add context files
79+
for path, content in context_files.items():
80+
prompt_parts.append(f"<|file_sep|>{path}")
81+
prompt_parts.append(content)
82+
83+
# Add recent diffs
84+
for diff in recent_diffs:
85+
prompt_parts.append(f"<|file_sep|>{diff['file_path']}.diff")
86+
prompt_parts.append("original:")
87+
prompt_parts.append(diff['original'])
88+
prompt_parts.append("updated:")
89+
prompt_parts.append(diff['updated'])
90+
91+
# Add original and current states
92+
prompt_parts.append(f"<|file_sep|>original/{file_path}")
93+
prompt_parts.append(original_content)
94+
prompt_parts.append(f"<|file_sep|>current/{file_path}")
95+
prompt_parts.append(current_content)
96+
prompt_parts.append(f"<|file_sep|>updated/{file_path}")
97+
98+
return "\n".join(prompt_parts)
99+
100+
101+
def predict(input_data: dict) -> dict:
102+
"""Run prediction using the Sweep model."""
103+
try:
104+
from llama_cpp import Llama
105+
except ImportError:
106+
return {
107+
"error": "llama_cpp not installed",
108+
"message": "Run: pip install llama-cpp-python"
109+
}
110+
111+
model_path = get_model_path()
112+
113+
# Build prompt
114+
prompt = build_prompt(
115+
context_files=input_data.get("context_files", {}),
116+
recent_diffs=input_data.get("recent_diffs", []),
117+
file_path=input_data["file_path"],
118+
original_content=input_data.get("original_content", input_data["current_content"]),
119+
current_content=input_data["current_content"],
120+
)
121+
122+
# Load model and generate
123+
try:
124+
llm = Llama(
125+
model_path=str(model_path),
126+
n_ctx=8192,
127+
n_threads=os.cpu_count() or 4,
128+
verbose=False
129+
)
130+
131+
import time
132+
start_time = time.time()
133+
134+
output = llm(
135+
prompt,
136+
max_tokens=input_data.get("max_tokens", 512),
137+
temperature=input_data.get("temperature", 0.0),
138+
stop=["<|file_sep|>", "</s>"],
139+
)
140+
141+
end_time = time.time()
142+
143+
predicted_content = output["choices"][0]["text"]
144+
145+
return {
146+
"success": True,
147+
"predicted_content": predicted_content,
148+
"file_path": input_data["file_path"],
149+
"latency_ms": int((end_time - start_time) * 1000),
150+
"tokens_generated": output["usage"]["completion_tokens"]
151+
}
152+
153+
except Exception as e:
154+
return {
155+
"error": "prediction_failed",
156+
"message": str(e)
157+
}
158+
159+
160+
def main():
161+
"""Main entry point - reads JSON from stdin, outputs prediction to stdout."""
162+
try:
163+
# Read input from stdin
164+
input_text = sys.stdin.read()
165+
if not input_text.strip():
166+
print(json.dumps({"error": "no_input", "message": "No input provided"}))
167+
sys.exit(1)
168+
169+
input_data = json.loads(input_text)
170+
171+
# Validate required fields
172+
if "file_path" not in input_data:
173+
print(json.dumps({"error": "missing_field", "message": "file_path is required"}))
174+
sys.exit(1)
175+
if "current_content" not in input_data:
176+
print(json.dumps({"error": "missing_field", "message": "current_content is required"}))
177+
sys.exit(1)
178+
179+
# Run prediction
180+
result = predict(input_data)
181+
print(json.dumps(result))
182+
183+
except json.JSONDecodeError as e:
184+
print(json.dumps({"error": "invalid_json", "message": str(e)}))
185+
sys.exit(1)
186+
except Exception as e:
187+
print(json.dumps({"error": "unexpected", "message": str(e)}))
188+
sys.exit(1)
189+
190+
191+
if __name__ == "__main__":
192+
main()

0 commit comments

Comments
 (0)