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.
Files changed (44) hide show
  1. promptscan/__init__.py +142 -0
  2. promptscan/batch_importer.py +401 -0
  3. promptscan/cli.py +1349 -0
  4. promptscan/config.py +331 -0
  5. promptscan/data_utils.py +295 -0
  6. promptscan/detector.py +677 -0
  7. promptscan/ensemble/__init__.py +8 -0
  8. promptscan/ensemble/detector.py +194 -0
  9. promptscan/ensemble/voting.py +124 -0
  10. promptscan/feedback_store.py +433 -0
  11. promptscan/github_client.py +449 -0
  12. promptscan/models/__init__.py +10 -0
  13. promptscan/models/base_model.py +64 -0
  14. promptscan/models/checkpoints/cnn_best.pt +0 -0
  15. promptscan/models/checkpoints/lstm_best.pt +0 -0
  16. promptscan/models/cnn_model.py +171 -0
  17. promptscan/models/lstm_model.py +150 -0
  18. promptscan/models/transformer_model.py +206 -0
  19. promptscan/parquet_store.py +354 -0
  20. promptscan/processors/__init__.py +8 -0
  21. promptscan/processors/subword_processor.py +81 -0
  22. promptscan/processors/word_processor.py +97 -0
  23. promptscan/text_extractors.py +666 -0
  24. promptscan/training/__init__.py +26 -0
  25. promptscan/training/base_trainer.py +526 -0
  26. promptscan/training/data_loader.py +234 -0
  27. promptscan/training/pipeline.py +463 -0
  28. promptscan/training/strategies/__init__.py +16 -0
  29. promptscan/training/strategies/cnn_strategy.py +115 -0
  30. promptscan/training/strategies/lstm_strategy.py +110 -0
  31. promptscan/training/strategies/transformer_strategy.py +123 -0
  32. promptscan/unified_detector.py +100 -0
  33. promptscan/utils/__init__.py +7 -0
  34. promptscan/utils/data_loader.py +35 -0
  35. promptscan/utils/device.py +162 -0
  36. promptscan/utils/markdown_parser.py +245 -0
  37. promptscan/utils/memory_monitor.py +219 -0
  38. promptscan/utils/text_processor.py +83 -0
  39. promptscan-0.1.1.dist-info/METADATA +483 -0
  40. promptscan-0.1.1.dist-info/RECORD +44 -0
  41. promptscan-0.1.1.dist-info/WHEEL +5 -0
  42. promptscan-0.1.1.dist-info/entry_points.txt +7 -0
  43. promptscan-0.1.1.dist-info/licenses/LICENSE +21 -0
  44. 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}%)")