promptscan 0.1.1__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.
- promptscan/__init__.py +142 -0
- promptscan/batch_importer.py +401 -0
- promptscan/cli.py +1349 -0
- promptscan/config.py +331 -0
- promptscan/data_utils.py +295 -0
- promptscan/detector.py +677 -0
- promptscan/ensemble/__init__.py +8 -0
- promptscan/ensemble/detector.py +194 -0
- promptscan/ensemble/voting.py +124 -0
- promptscan/feedback_store.py +433 -0
- promptscan/github_client.py +449 -0
- promptscan/models/__init__.py +10 -0
- promptscan/models/base_model.py +64 -0
- promptscan/models/checkpoints/cnn_best.pt +0 -0
- promptscan/models/checkpoints/lstm_best.pt +0 -0
- promptscan/models/cnn_model.py +171 -0
- promptscan/models/lstm_model.py +150 -0
- promptscan/models/transformer_model.py +206 -0
- promptscan/parquet_store.py +354 -0
- promptscan/processors/__init__.py +8 -0
- promptscan/processors/subword_processor.py +81 -0
- promptscan/processors/word_processor.py +97 -0
- promptscan/text_extractors.py +666 -0
- promptscan/training/__init__.py +26 -0
- promptscan/training/base_trainer.py +526 -0
- promptscan/training/data_loader.py +234 -0
- promptscan/training/pipeline.py +463 -0
- promptscan/training/strategies/__init__.py +16 -0
- promptscan/training/strategies/cnn_strategy.py +115 -0
- promptscan/training/strategies/lstm_strategy.py +110 -0
- promptscan/training/strategies/transformer_strategy.py +123 -0
- promptscan/unified_detector.py +100 -0
- promptscan/utils/__init__.py +7 -0
- promptscan/utils/data_loader.py +35 -0
- promptscan/utils/device.py +162 -0
- promptscan/utils/markdown_parser.py +245 -0
- promptscan/utils/memory_monitor.py +219 -0
- promptscan/utils/text_processor.py +83 -0
- promptscan-0.1.1.dist-info/METADATA +483 -0
- promptscan-0.1.1.dist-info/RECORD +44 -0
- promptscan-0.1.1.dist-info/WHEEL +5 -0
- promptscan-0.1.1.dist-info/entry_points.txt +7 -0
- promptscan-0.1.1.dist-info/licenses/LICENSE +21 -0
- promptscan-0.1.1.dist-info/top_level.txt +1 -0
promptscan/__init__.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Prompt Detective - AI-powered prompt injection detection system."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
__version__ = VERSION = "0.1.1"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_model_path(model_name: str) -> Path:
|
|
10
|
+
"""
|
|
11
|
+
Get the path to a model checkpoint file.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
model_name: Name of the model file (e.g., "cnn_best.pt", "transformer_best.pt")
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
Path to the model file
|
|
18
|
+
|
|
19
|
+
Raises:
|
|
20
|
+
FileNotFoundError: If model file is not found in any location
|
|
21
|
+
"""
|
|
22
|
+
# First check if the file exists at the given path (for custom models)
|
|
23
|
+
if os.path.exists(model_name):
|
|
24
|
+
return Path(model_name)
|
|
25
|
+
|
|
26
|
+
# Check environment variable for custom model directory
|
|
27
|
+
env_model_dir = os.environ.get("PROMPTSCAN_MODEL_DIR")
|
|
28
|
+
if env_model_dir:
|
|
29
|
+
env_path = Path(env_model_dir) / model_name
|
|
30
|
+
if env_path.exists():
|
|
31
|
+
return env_path
|
|
32
|
+
|
|
33
|
+
# Check in the package's checkpoints directory
|
|
34
|
+
package_dir = Path(__file__).parent
|
|
35
|
+
checkpoint_path = package_dir / "models" / "checkpoints" / model_name
|
|
36
|
+
|
|
37
|
+
if checkpoint_path.exists():
|
|
38
|
+
return checkpoint_path
|
|
39
|
+
|
|
40
|
+
# Fallback to the old models/ directory (for backward compatibility)
|
|
41
|
+
fallback_path = Path("models") / model_name
|
|
42
|
+
if fallback_path.exists():
|
|
43
|
+
return fallback_path
|
|
44
|
+
|
|
45
|
+
# Try to find project root and check models directory
|
|
46
|
+
# Look for pyproject.toml or setup.cfg to identify project root
|
|
47
|
+
current = Path.cwd()
|
|
48
|
+
project_root = None
|
|
49
|
+
|
|
50
|
+
# Search up to 3 parent directories for project root
|
|
51
|
+
for i in range(4):
|
|
52
|
+
check_dir = current
|
|
53
|
+
for _ in range(i):
|
|
54
|
+
check_dir = check_dir.parent
|
|
55
|
+
|
|
56
|
+
if (check_dir / "pyproject.toml").exists() or (
|
|
57
|
+
check_dir / "setup.cfg"
|
|
58
|
+
).exists():
|
|
59
|
+
project_root = check_dir
|
|
60
|
+
break
|
|
61
|
+
|
|
62
|
+
if project_root:
|
|
63
|
+
project_model_path = project_root / "models" / model_name
|
|
64
|
+
if project_model_path.exists():
|
|
65
|
+
return project_model_path
|
|
66
|
+
|
|
67
|
+
# If we get here, no model was found
|
|
68
|
+
searched_paths = [
|
|
69
|
+
f"1. Direct path: {model_name}",
|
|
70
|
+
f"2. Package checkpoints: {checkpoint_path}",
|
|
71
|
+
f"3. Local models directory: {fallback_path}",
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
if env_model_dir:
|
|
75
|
+
searched_paths.append(
|
|
76
|
+
f"4. Environment variable PROMPTSCAN_MODEL_DIR: {env_path}"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
if project_root:
|
|
80
|
+
searched_paths.append(f"5. Project root models: {project_model_path}")
|
|
81
|
+
|
|
82
|
+
raise FileNotFoundError(
|
|
83
|
+
f"Model '{model_name}' not found.\n"
|
|
84
|
+
f"Searched locations:\n" + "\n".join(searched_paths) + "\n\n"
|
|
85
|
+
f"To fix:\n"
|
|
86
|
+
f" 1. Train a model: promptscan train --model-type cnn|lstm|transformer\n"
|
|
87
|
+
f" 2. Specify custom path with --model\n"
|
|
88
|
+
f" 3. Check if models exist in expected locations\n"
|
|
89
|
+
f" 4. Set PROMPTSCAN_MODEL_DIR environment variable"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def get_default_model_save_path(model_name: str) -> Path:
|
|
94
|
+
"""
|
|
95
|
+
Get the default path where a model should be saved.
|
|
96
|
+
|
|
97
|
+
This is similar to get_model_path() but doesn't check if the file exists.
|
|
98
|
+
Used by training commands to determine where to save new models.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
model_name: Name of the model file (e.g., "cnn_best.pt", "transformer_best.pt")
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Path where the model should be saved
|
|
105
|
+
"""
|
|
106
|
+
# First check if it looks like an absolute or relative path
|
|
107
|
+
if os.path.isabs(model_name) or "/" in model_name or "\\" in model_name:
|
|
108
|
+
return Path(model_name)
|
|
109
|
+
|
|
110
|
+
# Default to saving in the local models directory
|
|
111
|
+
return Path("models") / model_name
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def get_default_model_paths() -> dict:
|
|
115
|
+
"""Get default paths for all model checkpoints."""
|
|
116
|
+
return {
|
|
117
|
+
"cnn": get_model_path("cnn_best.pt"),
|
|
118
|
+
"lstm": get_model_path("lstm_best.pt"),
|
|
119
|
+
"transformer": get_model_path("transformer_best.pt"),
|
|
120
|
+
"best": get_model_path("best_model.pt"),
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# Import key components for easier access
|
|
125
|
+
try:
|
|
126
|
+
from .unified_detector import UnifiedDetector
|
|
127
|
+
from .feedback_store import ParquetFeedbackStore
|
|
128
|
+
from .parquet_store import ParquetDataStore
|
|
129
|
+
except ImportError:
|
|
130
|
+
# Allow partial imports for documentation generation
|
|
131
|
+
pass
|
|
132
|
+
|
|
133
|
+
__all__ = [
|
|
134
|
+
"__version__",
|
|
135
|
+
"VERSION",
|
|
136
|
+
"get_model_path",
|
|
137
|
+
"get_default_model_save_path",
|
|
138
|
+
"get_default_model_paths",
|
|
139
|
+
"UnifiedDetector",
|
|
140
|
+
"ParquetFeedbackStore",
|
|
141
|
+
"ParquetDataStore",
|
|
142
|
+
]
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Batch importer for prompt insertion with duplicate checking.
|
|
4
|
+
Supports local files, directories, and GitHub repositories.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import time
|
|
8
|
+
import os
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class ImportStats:
|
|
16
|
+
"""Statistics for import operations."""
|
|
17
|
+
|
|
18
|
+
total_files: int = 0
|
|
19
|
+
processed: int = 0
|
|
20
|
+
skipped: int = 0
|
|
21
|
+
failed: int = 0
|
|
22
|
+
added: int = 0
|
|
23
|
+
duplicates: int = 0
|
|
24
|
+
start_time: float = 0.0
|
|
25
|
+
end_time: float = 0.0
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def duration(self) -> float:
|
|
29
|
+
"""Get import duration in seconds."""
|
|
30
|
+
if self.end_time and self.start_time:
|
|
31
|
+
return self.end_time - self.start_time
|
|
32
|
+
return 0.0
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def success_rate(self) -> float:
|
|
36
|
+
"""Get success rate as percentage."""
|
|
37
|
+
if self.total_files == 0:
|
|
38
|
+
return 0.0
|
|
39
|
+
return (self.processed - self.failed) / self.total_files * 100
|
|
40
|
+
|
|
41
|
+
def start(self):
|
|
42
|
+
"""Start timing."""
|
|
43
|
+
self.start_time = time.time()
|
|
44
|
+
|
|
45
|
+
def finish(self):
|
|
46
|
+
"""Finish timing."""
|
|
47
|
+
self.end_time = time.time()
|
|
48
|
+
|
|
49
|
+
def __str__(self) -> str:
|
|
50
|
+
"""String representation."""
|
|
51
|
+
return (
|
|
52
|
+
f"Processed: {self.processed}/{self.total_files} files, "
|
|
53
|
+
f"Added: {self.added} prompts, "
|
|
54
|
+
f"Skipped: {self.skipped} files, "
|
|
55
|
+
f"Duplicates: {self.duplicates} prompts, "
|
|
56
|
+
f"Failed: {self.failed} files, "
|
|
57
|
+
f"Duration: {self.duration:.2f}s"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class BatchImporter:
|
|
62
|
+
"""Batch importer for prompt data with duplicate checking."""
|
|
63
|
+
|
|
64
|
+
def __init__(self, store, verbose=False, github_token=None):
|
|
65
|
+
"""
|
|
66
|
+
Initialize batch importer.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
store: ParquetDataStore instance
|
|
70
|
+
verbose: Show verbose output
|
|
71
|
+
github_token: GitHub token for API access
|
|
72
|
+
"""
|
|
73
|
+
self.store = store
|
|
74
|
+
self.verbose = verbose
|
|
75
|
+
self.github_token = github_token
|
|
76
|
+
|
|
77
|
+
def import_from_files(
|
|
78
|
+
self,
|
|
79
|
+
files: List[Union[str, Path]],
|
|
80
|
+
label: Optional[bool] = None,
|
|
81
|
+
dry_run: bool = False,
|
|
82
|
+
verbose: bool = False,
|
|
83
|
+
) -> ImportStats:
|
|
84
|
+
"""
|
|
85
|
+
Import prompts from files.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
files: List of file paths (strings or Path objects)
|
|
89
|
+
label: Label for all prompts (True=injection, False=safe, None=interactive)
|
|
90
|
+
dry_run: Preview without importing
|
|
91
|
+
verbose: Show verbose output
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Import statistics
|
|
95
|
+
"""
|
|
96
|
+
stats = ImportStats()
|
|
97
|
+
stats.start()
|
|
98
|
+
stats.total_files = len(files)
|
|
99
|
+
|
|
100
|
+
print(f"š Importing {len(files)} files")
|
|
101
|
+
print(
|
|
102
|
+
f" Label: {'injection' if label else 'safe' if label is not None else 'interactive'}"
|
|
103
|
+
)
|
|
104
|
+
print(f" Dry run: {dry_run}")
|
|
105
|
+
|
|
106
|
+
prompts = []
|
|
107
|
+
|
|
108
|
+
for i, filepath in enumerate(files, 1):
|
|
109
|
+
# Convert to Path object if it's a string
|
|
110
|
+
filepath = Path(filepath) if isinstance(filepath, str) else filepath
|
|
111
|
+
|
|
112
|
+
if verbose or i % max(1, len(files) // 20) == 0 or i == len(files):
|
|
113
|
+
print(f" [{i}/{len(files)}] Processing: {filepath.name[:50]}...")
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
# Check if file exists
|
|
117
|
+
if not filepath.exists():
|
|
118
|
+
if verbose:
|
|
119
|
+
print(f" ā File not found: {filepath}")
|
|
120
|
+
stats.skipped += 1
|
|
121
|
+
continue
|
|
122
|
+
|
|
123
|
+
# Read file content
|
|
124
|
+
content = filepath.read_text(encoding="utf-8", errors="ignore").strip()
|
|
125
|
+
if not content:
|
|
126
|
+
if verbose:
|
|
127
|
+
print(f" ā ļø Empty file: {filepath.name}")
|
|
128
|
+
stats.skipped += 1
|
|
129
|
+
continue
|
|
130
|
+
|
|
131
|
+
# Create prompt data
|
|
132
|
+
prompt_data = {
|
|
133
|
+
"text": content,
|
|
134
|
+
"is_injection": label if label is not None else False,
|
|
135
|
+
"source": str(filepath),
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
prompts.append(prompt_data)
|
|
139
|
+
stats.processed += 1
|
|
140
|
+
|
|
141
|
+
except Exception as e:
|
|
142
|
+
if verbose:
|
|
143
|
+
print(f" ā Error processing {filepath.name}: {e}")
|
|
144
|
+
stats.failed += 1
|
|
145
|
+
continue
|
|
146
|
+
|
|
147
|
+
# Batch insert every 100 prompts
|
|
148
|
+
if len(prompts) >= 100:
|
|
149
|
+
if not dry_run:
|
|
150
|
+
added_ids, duplicates = self.store.add_prompts_batch(prompts)
|
|
151
|
+
stats.added += len(added_ids)
|
|
152
|
+
stats.duplicates += duplicates
|
|
153
|
+
else:
|
|
154
|
+
stats.added += len(prompts)
|
|
155
|
+
prompts = []
|
|
156
|
+
|
|
157
|
+
# Insert remaining prompts
|
|
158
|
+
if prompts:
|
|
159
|
+
if not dry_run:
|
|
160
|
+
added_ids, duplicates = self.store.add_prompts_batch(prompts)
|
|
161
|
+
stats.added += len(added_ids)
|
|
162
|
+
stats.duplicates += duplicates
|
|
163
|
+
else:
|
|
164
|
+
stats.added += len(prompts)
|
|
165
|
+
|
|
166
|
+
stats.finish()
|
|
167
|
+
|
|
168
|
+
# Print summary
|
|
169
|
+
self._print_import_summary(stats, dry_run)
|
|
170
|
+
|
|
171
|
+
return stats
|
|
172
|
+
|
|
173
|
+
def import_from_directory(
|
|
174
|
+
self,
|
|
175
|
+
directory: Union[str, Path],
|
|
176
|
+
label: Optional[bool] = None,
|
|
177
|
+
extensions: Optional[List[str]] = None,
|
|
178
|
+
exclude: Optional[List[str]] = None,
|
|
179
|
+
max_size: Optional[int] = None,
|
|
180
|
+
recursive: bool = True,
|
|
181
|
+
dry_run: bool = False,
|
|
182
|
+
verbose: bool = False,
|
|
183
|
+
) -> ImportStats:
|
|
184
|
+
"""
|
|
185
|
+
Import prompts from directory.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
directory: Directory path (string or Path)
|
|
189
|
+
label: Label for all prompts
|
|
190
|
+
extensions: File extensions to include (e.g., ['.txt', '.md', '.py', '.js'])
|
|
191
|
+
Default includes common text and source code files
|
|
192
|
+
exclude: Path patterns to exclude
|
|
193
|
+
max_size: Maximum file size in bytes (None for no limit)
|
|
194
|
+
recursive: Whether to traverse subdirectories
|
|
195
|
+
dry_run: Preview without importing
|
|
196
|
+
verbose: Show verbose output
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
Import statistics
|
|
200
|
+
"""
|
|
201
|
+
# Convert to Path object if it's a string
|
|
202
|
+
directory = Path(directory) if isinstance(directory, str) else directory
|
|
203
|
+
|
|
204
|
+
if not directory.exists():
|
|
205
|
+
raise ValueError(f"Directory not found: {directory}")
|
|
206
|
+
|
|
207
|
+
if not directory.is_dir():
|
|
208
|
+
raise ValueError(f"Path is not a directory: {directory}")
|
|
209
|
+
|
|
210
|
+
# Default extensions
|
|
211
|
+
if extensions is None:
|
|
212
|
+
extensions = [
|
|
213
|
+
".txt",
|
|
214
|
+
".md",
|
|
215
|
+
".py",
|
|
216
|
+
".json",
|
|
217
|
+
".csv",
|
|
218
|
+
".yaml",
|
|
219
|
+
".yml",
|
|
220
|
+
".xml",
|
|
221
|
+
".html",
|
|
222
|
+
".js",
|
|
223
|
+
".jsx",
|
|
224
|
+
".ts",
|
|
225
|
+
".tsx",
|
|
226
|
+
".rs",
|
|
227
|
+
".go",
|
|
228
|
+
".java",
|
|
229
|
+
".cpp",
|
|
230
|
+
".c",
|
|
231
|
+
".h",
|
|
232
|
+
".hpp",
|
|
233
|
+
".cs",
|
|
234
|
+
".php",
|
|
235
|
+
".rb",
|
|
236
|
+
".pl",
|
|
237
|
+
".pm",
|
|
238
|
+
".sh",
|
|
239
|
+
".bash",
|
|
240
|
+
".zsh",
|
|
241
|
+
".fish",
|
|
242
|
+
".ps1",
|
|
243
|
+
".bat",
|
|
244
|
+
".cmd",
|
|
245
|
+
".sql",
|
|
246
|
+
".r",
|
|
247
|
+
".m",
|
|
248
|
+
".swift",
|
|
249
|
+
".kt",
|
|
250
|
+
".kts",
|
|
251
|
+
".scala",
|
|
252
|
+
".clj",
|
|
253
|
+
".cljs",
|
|
254
|
+
".lua",
|
|
255
|
+
".erl",
|
|
256
|
+
".ex",
|
|
257
|
+
".exs",
|
|
258
|
+
".hs",
|
|
259
|
+
".ml",
|
|
260
|
+
".mli",
|
|
261
|
+
".fs",
|
|
262
|
+
".fsx",
|
|
263
|
+
".v",
|
|
264
|
+
".sv",
|
|
265
|
+
".vhd",
|
|
266
|
+
".vhdl",
|
|
267
|
+
".asm",
|
|
268
|
+
".s",
|
|
269
|
+
".ino",
|
|
270
|
+
".ino.cpp",
|
|
271
|
+
]
|
|
272
|
+
|
|
273
|
+
print(f"š Found directory: {directory}")
|
|
274
|
+
print(f" Extensions: {', '.join(extensions)}")
|
|
275
|
+
print(f" Recursive: {recursive}")
|
|
276
|
+
print(
|
|
277
|
+
f" Max file size: {'unlimited' if max_size is None else f'{max_size:,} bytes'}"
|
|
278
|
+
)
|
|
279
|
+
print(
|
|
280
|
+
f" Label: {'injection' if label else 'safe' if label is not None else 'interactive'}"
|
|
281
|
+
)
|
|
282
|
+
print(f" Dry run: {dry_run}")
|
|
283
|
+
|
|
284
|
+
# Find files
|
|
285
|
+
files = []
|
|
286
|
+
for ext in extensions:
|
|
287
|
+
if recursive:
|
|
288
|
+
files.extend(directory.rglob(f"*{ext}"))
|
|
289
|
+
else:
|
|
290
|
+
files.extend(directory.glob(f"*{ext}"))
|
|
291
|
+
|
|
292
|
+
# Filter out excluded patterns
|
|
293
|
+
if exclude:
|
|
294
|
+
filtered_files = []
|
|
295
|
+
for file in files:
|
|
296
|
+
file_str = str(file)
|
|
297
|
+
if not any(pattern in file_str for pattern in exclude):
|
|
298
|
+
filtered_files.append(file)
|
|
299
|
+
files = filtered_files
|
|
300
|
+
|
|
301
|
+
# Filter by file size if max_size is specified
|
|
302
|
+
if max_size is not None:
|
|
303
|
+
size_filtered_files = []
|
|
304
|
+
for file in files:
|
|
305
|
+
try:
|
|
306
|
+
if file.stat().st_size <= max_size:
|
|
307
|
+
size_filtered_files.append(file)
|
|
308
|
+
elif verbose:
|
|
309
|
+
print(
|
|
310
|
+
f" ā ļø Skipping large file: {file.name} ({file.stat().st_size:,} bytes)"
|
|
311
|
+
)
|
|
312
|
+
except (OSError, IOError):
|
|
313
|
+
# Skip files we can't stat
|
|
314
|
+
if verbose:
|
|
315
|
+
print(f" ā ļø Cannot check size: {file.name}")
|
|
316
|
+
size_filtered_files.append(file)
|
|
317
|
+
files = size_filtered_files
|
|
318
|
+
|
|
319
|
+
print(f"š Found {len(files)} files to process")
|
|
320
|
+
|
|
321
|
+
# Import files
|
|
322
|
+
return self.import_from_files(files, label, dry_run, verbose or self.verbose)
|
|
323
|
+
|
|
324
|
+
def import_from_github(
|
|
325
|
+
self,
|
|
326
|
+
github_url: str,
|
|
327
|
+
label: Optional[bool] = None,
|
|
328
|
+
extensions: Optional[List[str]] = None,
|
|
329
|
+
exclude: Optional[List[str]] = None,
|
|
330
|
+
max_size: Optional[int] = None,
|
|
331
|
+
max_depth: int = 10,
|
|
332
|
+
dry_run: bool = False,
|
|
333
|
+
github_token: Optional[str] = None,
|
|
334
|
+
branch: str = "main",
|
|
335
|
+
) -> ImportStats:
|
|
336
|
+
"""
|
|
337
|
+
Import prompts from GitHub repository.
|
|
338
|
+
|
|
339
|
+
Note: This is a simplified stub. For full GitHub import functionality,
|
|
340
|
+
use the original batch_importer.py or implement GitHub API integration.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
github_url: GitHub repository URL
|
|
344
|
+
label: Label for all prompts
|
|
345
|
+
extensions: File extensions to include
|
|
346
|
+
exclude: Path patterns to exclude
|
|
347
|
+
max_size: Maximum file size in bytes
|
|
348
|
+
max_depth: Maximum directory depth
|
|
349
|
+
dry_run: Preview without importing
|
|
350
|
+
github_token: GitHub personal access token
|
|
351
|
+
branch: Git branch to use
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
Import statistics
|
|
355
|
+
"""
|
|
356
|
+
print(f"š GitHub Import: {github_url}")
|
|
357
|
+
print(f"šæ Branch: {branch}")
|
|
358
|
+
print(f"š Max depth: {max_depth}")
|
|
359
|
+
|
|
360
|
+
# Show helpful message about limited functionality
|
|
361
|
+
print("\nā ļø NOTE: Simplified GitHub import has limited functionality.")
|
|
362
|
+
print(" For full GitHub import features:")
|
|
363
|
+
print(" 1. Use the original batch_importer.py with GitHub API integration")
|
|
364
|
+
print(" 2. Or clone the repository locally and use --dir option")
|
|
365
|
+
print(" 3. Or implement proper GitHub API support")
|
|
366
|
+
|
|
367
|
+
# Create empty stats to return
|
|
368
|
+
stats = ImportStats()
|
|
369
|
+
stats.start()
|
|
370
|
+
|
|
371
|
+
print(f"\nš GitHub import would process repository: {github_url}")
|
|
372
|
+
print(" (Actual import not implemented in simplified version)")
|
|
373
|
+
|
|
374
|
+
stats.finish()
|
|
375
|
+
self._print_import_summary(stats, dry_run)
|
|
376
|
+
|
|
377
|
+
return stats
|
|
378
|
+
|
|
379
|
+
def _print_import_summary(self, stats: ImportStats, dry_run: bool = False):
|
|
380
|
+
"""Print import summary statistics."""
|
|
381
|
+
print(f"\nš IMPORT COMPLETE")
|
|
382
|
+
print(f" Total files: {stats.total_files}")
|
|
383
|
+
print(f" Processed: {stats.processed}")
|
|
384
|
+
print(f" Added: {stats.added}")
|
|
385
|
+
print(f" Skipped: {stats.skipped}")
|
|
386
|
+
print(f" Failed: {stats.failed}")
|
|
387
|
+
print(f" Duplicates: {stats.duplicates}")
|
|
388
|
+
print(f" Success rate: {stats.success_rate:.1f}%")
|
|
389
|
+
print(f" Duration: {stats.duration:.2f}s")
|
|
390
|
+
|
|
391
|
+
# Show database stats if not dry run and something was added
|
|
392
|
+
if not dry_run and stats.added > 0:
|
|
393
|
+
all_prompts = self.store.get_all_prompts()
|
|
394
|
+
injections = sum(1 for p in all_prompts if p.get("is_injection"))
|
|
395
|
+
safe = len(all_prompts) - injections
|
|
396
|
+
|
|
397
|
+
print(f"\nš¾ Database now contains {len(all_prompts)} prompts:")
|
|
398
|
+
print(
|
|
399
|
+
f" š“ {injections} injection prompts ({injections / len(all_prompts) * 100:.1f}%)"
|
|
400
|
+
)
|
|
401
|
+
print(f" š¢ {safe} safe prompts ({safe / len(all_prompts) * 100:.1f}%)")
|