prellm 0.1.12__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.
prellm-0.1.12/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Softreck
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
prellm-0.1.12/PKG-INFO ADDED
@@ -0,0 +1,141 @@
1
+ Metadata-Version: 2.4
2
+ Name: prellm
3
+ Version: 0.1.12
4
+ Summary: PromptGuard - Lightweight LLM prompt middleware for bias detection, standardization, and DevOps process chains via YAML config, supporting context injection and multi-provider orchestration.
5
+ License: Apache-2.0
6
+ License-File: LICENSE
7
+ Keywords: llm,prompt-engineering,bias-detection,devops,litellm
8
+ Author: Softreck
9
+ Author-email: tom@sapletta.com
10
+ Requires-Python: >=3.10,<4.0
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: Apache Software License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Requires-Dist: litellm (>=1.40,<2.0)
21
+ Requires-Dist: nltk (>=3.8,<4.0)
22
+ Requires-Dist: pydantic (>=2.0,<3.0)
23
+ Requires-Dist: pyyaml (>=6.0,<7.0)
24
+ Requires-Dist: textstat (>=0.7,<0.8)
25
+ Requires-Dist: typer[all] (>=0.12,<0.13)
26
+ Project-URL: Repository, https://github.com/softreck/gllm
27
+ Description-Content-Type: text/markdown
28
+
29
+ # 🛡️ PromptGuard
30
+
31
+ **Lightweight LLM prompt middleware — bias detection, standardization, and DevOps process chains via YAML config.**
32
+
33
+ PromptGuard sits between your application and LLM providers, automatically detecting bias, ambiguity, and dangerous patterns in prompts. It enriches queries with context, validates outputs, and supports multi-step DevOps workflows with approval gates.
34
+
35
+ ## Features
36
+
37
+ - **Bias & Ambiguity Detection** — regex + NLTK patterns for PL/EN, with DevOps-specific guardrails
38
+ - **YAML-Driven Config** — declarative rules, clarification templates, model fallbacks
39
+ - **100+ LLM Models** — via LiteLLM proxy (OpenAI, Anthropic, Llama, Mistral, etc.)
40
+ - **DevOps Process Chains** — multi-step workflows with approval gates, rollback, and audit trails
41
+ - **Context Injection** — auto-enrich prompts with env vars, git info, system state
42
+ - **Type-Safe Outputs** — Pydantic v2 validated responses
43
+ - **Lightweight** — <50MB, 5 dependencies, async-first
44
+
45
+ ## Quick Start
46
+
47
+ ```bash
48
+ # Install
49
+ pip install promptguard
50
+
51
+ # Generate config
52
+ promptguard init --devops -o rules.yaml
53
+
54
+ # Analyze a query (no LLM call)
55
+ promptguard analyze "Deploy to production" --config rules.yaml
56
+
57
+ # Run with LLM
58
+ promptguard run "Zdeployuj na staging" --config rules.yaml --model gpt-4o-mini
59
+
60
+ # Execute a process chain
61
+ promptguard process deploy.yaml --guard-config rules.yaml --env production
62
+ ```
63
+
64
+ ## Python API
65
+
66
+ ```python
67
+ from promptguard import PromptGuard, ProcessChain
68
+
69
+ # Simple query
70
+ guard = PromptGuard("rules.yaml")
71
+ result = await guard("Deploy to production", model="gpt-4o-mini")
72
+ print(result.clarified) # True — detected missing context
73
+ print(result.content) # Enriched response
74
+
75
+ # Process chain
76
+ chain = ProcessChain("deploy.yaml")
77
+ result = await chain.execute(env="production", dry_run=True)
78
+ for step in result.steps:
79
+ print(f"{step.step_name}: {step.status}")
80
+ ```
81
+
82
+ ## Configuration
83
+
84
+ ### rules.yaml
85
+ ```yaml
86
+ bias_patterns:
87
+ - regex: "(deploy|zdeployuj)\\s+(na|to)\\s+(prod|production)"
88
+ action: clarify
89
+ severity: critical
90
+ description: "Production deployment — requires context"
91
+
92
+ clarify_template: "[KONTEKST]: Podaj szczegóły dla: {query}"
93
+ max_retries: 3
94
+ policy: devops
95
+
96
+ models:
97
+ fallback: ["gpt-4o-mini", "llama3"]
98
+
99
+ context_sources:
100
+ - env: [CLUSTER, NAMESPACE, GIT_SHA]
101
+ - git: [branch, short_sha]
102
+ ```
103
+
104
+ ### deploy.yaml (Process Chain)
105
+ ```yaml
106
+ process: deploy-production
107
+ steps:
108
+ - name: pre-check
109
+ prompt: "Check readiness of {CLUSTER}"
110
+ approval: auto
111
+ - name: deploy
112
+ prompt: "Rolling deploy to {CLUSTER}/{NAMESPACE}"
113
+ approval: manual
114
+ rollback: true
115
+ ```
116
+
117
+ ## Architecture
118
+
119
+ ```
120
+ User Query → BiasDetector → ContextEngine → Enrichment → LiteLLM → Pydantic Validation → Response
121
+
122
+ ProcessChain → Approval Gates → Audit Trail
123
+ ```
124
+
125
+ ## Development
126
+
127
+ ```bash
128
+ git clone https://github.com/softreck/promptguard
129
+ cd promptguard
130
+ poetry install
131
+ poetry run pytest
132
+ ```
133
+
134
+ ## License
135
+
136
+ Apache License 2.0 - see [LICENSE](LICENSE) for details.
137
+
138
+ ## Author
139
+
140
+ Created by **Tom Sapletta** - [tom@sapletta.com](mailto:tom@sapletta.com)
141
+
@@ -0,0 +1,112 @@
1
+ # 🛡️ PromptGuard
2
+
3
+ **Lightweight LLM prompt middleware — bias detection, standardization, and DevOps process chains via YAML config.**
4
+
5
+ PromptGuard sits between your application and LLM providers, automatically detecting bias, ambiguity, and dangerous patterns in prompts. It enriches queries with context, validates outputs, and supports multi-step DevOps workflows with approval gates.
6
+
7
+ ## Features
8
+
9
+ - **Bias & Ambiguity Detection** — regex + NLTK patterns for PL/EN, with DevOps-specific guardrails
10
+ - **YAML-Driven Config** — declarative rules, clarification templates, model fallbacks
11
+ - **100+ LLM Models** — via LiteLLM proxy (OpenAI, Anthropic, Llama, Mistral, etc.)
12
+ - **DevOps Process Chains** — multi-step workflows with approval gates, rollback, and audit trails
13
+ - **Context Injection** — auto-enrich prompts with env vars, git info, system state
14
+ - **Type-Safe Outputs** — Pydantic v2 validated responses
15
+ - **Lightweight** — <50MB, 5 dependencies, async-first
16
+
17
+ ## Quick Start
18
+
19
+ ```bash
20
+ # Install
21
+ pip install promptguard
22
+
23
+ # Generate config
24
+ promptguard init --devops -o rules.yaml
25
+
26
+ # Analyze a query (no LLM call)
27
+ promptguard analyze "Deploy to production" --config rules.yaml
28
+
29
+ # Run with LLM
30
+ promptguard run "Zdeployuj na staging" --config rules.yaml --model gpt-4o-mini
31
+
32
+ # Execute a process chain
33
+ promptguard process deploy.yaml --guard-config rules.yaml --env production
34
+ ```
35
+
36
+ ## Python API
37
+
38
+ ```python
39
+ from promptguard import PromptGuard, ProcessChain
40
+
41
+ # Simple query
42
+ guard = PromptGuard("rules.yaml")
43
+ result = await guard("Deploy to production", model="gpt-4o-mini")
44
+ print(result.clarified) # True — detected missing context
45
+ print(result.content) # Enriched response
46
+
47
+ # Process chain
48
+ chain = ProcessChain("deploy.yaml")
49
+ result = await chain.execute(env="production", dry_run=True)
50
+ for step in result.steps:
51
+ print(f"{step.step_name}: {step.status}")
52
+ ```
53
+
54
+ ## Configuration
55
+
56
+ ### rules.yaml
57
+ ```yaml
58
+ bias_patterns:
59
+ - regex: "(deploy|zdeployuj)\\s+(na|to)\\s+(prod|production)"
60
+ action: clarify
61
+ severity: critical
62
+ description: "Production deployment — requires context"
63
+
64
+ clarify_template: "[KONTEKST]: Podaj szczegóły dla: {query}"
65
+ max_retries: 3
66
+ policy: devops
67
+
68
+ models:
69
+ fallback: ["gpt-4o-mini", "llama3"]
70
+
71
+ context_sources:
72
+ - env: [CLUSTER, NAMESPACE, GIT_SHA]
73
+ - git: [branch, short_sha]
74
+ ```
75
+
76
+ ### deploy.yaml (Process Chain)
77
+ ```yaml
78
+ process: deploy-production
79
+ steps:
80
+ - name: pre-check
81
+ prompt: "Check readiness of {CLUSTER}"
82
+ approval: auto
83
+ - name: deploy
84
+ prompt: "Rolling deploy to {CLUSTER}/{NAMESPACE}"
85
+ approval: manual
86
+ rollback: true
87
+ ```
88
+
89
+ ## Architecture
90
+
91
+ ```
92
+ User Query → BiasDetector → ContextEngine → Enrichment → LiteLLM → Pydantic Validation → Response
93
+
94
+ ProcessChain → Approval Gates → Audit Trail
95
+ ```
96
+
97
+ ## Development
98
+
99
+ ```bash
100
+ git clone https://github.com/softreck/promptguard
101
+ cd promptguard
102
+ poetry install
103
+ poetry run pytest
104
+ ```
105
+
106
+ ## License
107
+
108
+ Apache License 2.0 - see [LICENSE](LICENSE) for details.
109
+
110
+ ## Author
111
+
112
+ Created by **Tom Sapletta** - [tom@sapletta.com](mailto:tom@sapletta.com)
@@ -0,0 +1,19 @@
1
+ """PromptGuard — Lightweight LLM prompt middleware for bias detection, standardization, and DevOps process chains."""
2
+
3
+ __version__ = "0.1.12"
4
+
5
+ from promptguard.core import PromptGuard
6
+ from promptguard.models import GuardResponse, GuardConfig, AnalysisResult
7
+ from promptguard.chains.process_chain import ProcessChain
8
+ from promptguard.analyzers.bias_detector import BiasDetector
9
+ from promptguard.analyzers.context_engine import ContextEngine
10
+
11
+ __all__ = [
12
+ "PromptGuard",
13
+ "ProcessChain",
14
+ "GuardResponse",
15
+ "GuardConfig",
16
+ "AnalysisResult",
17
+ "BiasDetector",
18
+ "ContextEngine",
19
+ ]
@@ -0,0 +1,96 @@
1
+ """BiasDetector — scans queries for bias patterns, ambiguity, and readability issues."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from typing import Any
7
+
8
+ from promptguard.models import AnalysisResult, BiasPattern
9
+
10
+
11
+ # Default bias/ambiguity patterns for Polish & English DevOps context
12
+ DEFAULT_PATTERNS: list[dict[str, str]] = [
13
+ {"regex": r"(zawsze|always)\s+\w+", "action": "clarify", "severity": "medium", "description": "Absolute quantifier"},
14
+ {"regex": r"(tylko|only|just)\s+\w+", "action": "clarify", "severity": "low", "description": "Exclusive quantifier"},
15
+ {"regex": r"(głupi|stupid|dumb)\s+\w+", "action": "flag", "severity": "high", "description": "Derogatory language"},
16
+ {"regex": r"(zdeployuj|deploy|push)\s+(na|to)\s+(prod|production)", "action": "clarify", "severity": "critical",
17
+ "description": "Production deployment without context"},
18
+ {"regex": r"(usuń|delete|drop|remove)\s+(baz[ęa]|database|db|table)", "action": "clarify", "severity": "critical",
19
+ "description": "Destructive DB operation"},
20
+ {"regex": r"(restart|reboot|kill)\s+(server|service|pod|container)", "action": "clarify", "severity": "high",
21
+ "description": "Service disruption command"},
22
+ {"regex": r"(migrate|migruj)\s+.*(?:prod|production)", "action": "clarify", "severity": "critical",
23
+ "description": "Production migration"},
24
+ {"regex": r"(skaluj|scale)\s+(down|up|to\s+\d+)", "action": "clarify", "severity": "high",
25
+ "description": "Scaling operation"},
26
+ {"regex": r"(zmień|change|update)\s+(config|konfigurację|env|secret)", "action": "clarify", "severity": "high",
27
+ "description": "Configuration change"},
28
+ ]
29
+
30
+
31
+ class BiasDetector:
32
+ """Detects bias, ambiguity, and dangerous patterns in queries.
33
+
34
+ Uses regex patterns (configurable via YAML) + optional NLTK readability scoring.
35
+ Designed for both general prompt safety and DevOps-specific guardrails.
36
+ """
37
+
38
+ def __init__(self, patterns: list[BiasPattern] | None = None):
39
+ if patterns:
40
+ self.patterns = patterns
41
+ else:
42
+ self.patterns = [BiasPattern(**p) for p in DEFAULT_PATTERNS]
43
+
44
+ self._readability_available = False
45
+ try:
46
+ import textstat # noqa: F401
47
+ self._readability_available = True
48
+ except ImportError:
49
+ pass
50
+
51
+ def analyze(self, query: str) -> AnalysisResult:
52
+ """Analyze a query for bias patterns and ambiguity."""
53
+ detected: list[str] = []
54
+ ambiguity_flags: list[str] = []
55
+ needs_clarify = False
56
+
57
+ for pattern in self.patterns:
58
+ if re.search(pattern.regex, query, re.IGNORECASE):
59
+ detected.append(f"[{pattern.severity}] {pattern.description}")
60
+ if pattern.action == "clarify":
61
+ needs_clarify = True
62
+ ambiguity_flags.append(pattern.description)
63
+
64
+ # Check for very short / context-free queries
65
+ word_count = len(query.split())
66
+ if word_count < 4:
67
+ needs_clarify = True
68
+ ambiguity_flags.append("Query too short — likely missing context")
69
+
70
+ # Check for missing subject/object in DevOps context
71
+ devops_verbs = ["deploy", "zdeployuj", "migrate", "migruj", "scale", "skaluj",
72
+ "restart", "delete", "usuń", "push", "rollback"]
73
+ has_devops_verb = any(v in query.lower() for v in devops_verbs)
74
+ has_target = any(t in query.lower() for t in [
75
+ "staging", "production", "prod", "dev", "test", "cluster", "namespace"])
76
+ if has_devops_verb and not has_target:
77
+ needs_clarify = True
78
+ ambiguity_flags.append("DevOps command without target environment")
79
+
80
+ # Readability score
81
+ readability = None
82
+ if self._readability_available:
83
+ try:
84
+ import textstat
85
+ readability = textstat.flesch_reading_ease(query)
86
+ except Exception:
87
+ pass
88
+
89
+ return AnalysisResult(
90
+ needs_clarify=needs_clarify,
91
+ detected_patterns=detected,
92
+ enriched_query="",
93
+ original_query=query,
94
+ readability_score=readability,
95
+ ambiguity_flags=ambiguity_flags,
96
+ )
@@ -0,0 +1,90 @@
1
+ """ContextEngine — gathers runtime context (env vars, git info, system state) for prompt enrichment."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import subprocess
7
+ from typing import Any
8
+
9
+
10
+ class ContextEngine:
11
+ """Collects context from environment, git, and system for prompt enrichment.
12
+
13
+ Used by both core PromptGuard (auto-inject context) and ProcessChain (step-level context).
14
+ """
15
+
16
+ def __init__(self, context_sources: list[dict[str, Any]] | None = None):
17
+ self.sources = context_sources or []
18
+
19
+ def gather(self) -> dict[str, str]:
20
+ """Gather all configured context into a flat dict."""
21
+ ctx: dict[str, str] = {}
22
+
23
+ for source in self.sources:
24
+ if "env" in source:
25
+ ctx.update(self._gather_env(source["env"]))
26
+ if "git" in source:
27
+ ctx.update(self._gather_git(source["git"]))
28
+ if "system" in source:
29
+ ctx.update(self._gather_system(source["system"]))
30
+
31
+ return ctx
32
+
33
+ def enrich_prompt(self, prompt: str, extra: dict[str, str] | None = None) -> str:
34
+ """Substitute {VARIABLE} placeholders in a prompt with gathered context."""
35
+ ctx = self.gather()
36
+ if extra:
37
+ ctx.update(extra)
38
+
39
+ enriched = prompt
40
+ for key, value in ctx.items():
41
+ enriched = enriched.replace(f"{{{key}}}", value)
42
+
43
+ return enriched
44
+
45
+ @staticmethod
46
+ def _gather_env(keys: list[str]) -> dict[str, str]:
47
+ result = {}
48
+ for key in keys:
49
+ val = os.environ.get(key, "")
50
+ if val:
51
+ result[key] = val
52
+ return result
53
+
54
+ @staticmethod
55
+ def _gather_git(fields: list[str]) -> dict[str, str]:
56
+ result = {}
57
+ git_commands = {
58
+ "branch": ["git", "rev-parse", "--abbrev-ref", "HEAD"],
59
+ "last_commit": ["git", "log", "-1", "--format=%H"],
60
+ "last_commit_msg": ["git", "log", "-1", "--format=%s"],
61
+ "short_sha": ["git", "rev-parse", "--short", "HEAD"],
62
+ "tag": ["git", "describe", "--tags", "--abbrev=0"],
63
+ "remote_url": ["git", "remote", "get-url", "origin"],
64
+ }
65
+ for field in fields:
66
+ cmd = git_commands.get(field)
67
+ if cmd:
68
+ try:
69
+ out = subprocess.run(cmd, capture_output=True, text=True, timeout=5)
70
+ if out.returncode == 0:
71
+ result[field] = out.stdout.strip()
72
+ except (subprocess.TimeoutExpired, FileNotFoundError):
73
+ pass
74
+ return result
75
+
76
+ @staticmethod
77
+ def _gather_system(fields: list[str]) -> dict[str, str]:
78
+ result = {}
79
+ import platform
80
+ system_map = {
81
+ "hostname": platform.node,
82
+ "os": platform.system,
83
+ "arch": platform.machine,
84
+ "python": platform.python_version,
85
+ }
86
+ for field in fields:
87
+ fn = system_map.get(field)
88
+ if fn:
89
+ result[field] = fn()
90
+ return result
File without changes
@@ -0,0 +1,226 @@
1
+ """ProcessChain — Multi-step DevOps workflow engine with approval gates and audit trail.
2
+
3
+ Defines workflows as YAML, validates each step through PromptGuard, supports
4
+ manual/auto approval, rollback, and full audit logging.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ import time
11
+ from datetime import datetime
12
+ from pathlib import Path
13
+ from typing import Any, Awaitable, Callable
14
+
15
+ import yaml
16
+
17
+ from promptguard.analyzers.context_engine import ContextEngine
18
+ from promptguard.core import PromptGuard
19
+ from promptguard.models import (
20
+ ApprovalMode,
21
+ AuditEntry,
22
+ GuardConfig,
23
+ ProcessConfig,
24
+ ProcessResult,
25
+ ProcessStep,
26
+ StepResult,
27
+ StepStatus,
28
+ )
29
+
30
+ logger = logging.getLogger("promptguard.chains")
31
+
32
+ # Type for approval callback: receives step info, returns (approved: bool, approved_by: str)
33
+ ApprovalCallback = Callable[[str, str], Awaitable[tuple[bool, str]]]
34
+
35
+
36
+ class ProcessChain:
37
+ """Execute multi-step DevOps workflows with PromptGuard validation at each step.
38
+
39
+ Usage:
40
+ chain = ProcessChain("deploy.yaml")
41
+ result = await chain.execute(
42
+ env="production",
43
+ approval_callback=my_slack_approval_fn,
44
+ )
45
+
46
+ Each step's prompt goes through PromptGuard's full pipeline (bias detection,
47
+ enrichment, LLM call, validation) before execution.
48
+ """
49
+
50
+ def __init__(
51
+ self,
52
+ config_path: str | Path | None = None,
53
+ config: ProcessConfig | None = None,
54
+ guard_config_path: str | Path | None = None,
55
+ guard: PromptGuard | None = None,
56
+ ):
57
+ if config:
58
+ self.process_config = config
59
+ elif config_path:
60
+ self.process_config = self._load_process_config(Path(config_path))
61
+ else:
62
+ raise ValueError("Either config_path or config must be provided")
63
+
64
+ self.guard = guard or PromptGuard(config_path=guard_config_path)
65
+ self.context_engine = ContextEngine(self.process_config.context_sources)
66
+ self.audit_log: list[AuditEntry] = []
67
+ self._step_results: dict[str, StepResult] = {}
68
+
69
+ async def execute(
70
+ self,
71
+ extra_context: dict[str, str] | None = None,
72
+ approval_callback: ApprovalCallback | None = None,
73
+ dry_run: bool = False,
74
+ **env_overrides: str,
75
+ ) -> ProcessResult:
76
+ """Execute the full process chain.
77
+
78
+ Args:
79
+ extra_context: Additional key-value context to inject into prompts.
80
+ approval_callback: Async function for manual approval steps.
81
+ Receives (step_name, enriched_prompt) → (approved, approved_by).
82
+ dry_run: If True, analyze prompts but don't call LLM.
83
+ **env_overrides: Override environment-level context (e.g., env="production").
84
+
85
+ Returns:
86
+ ProcessResult with status of all steps.
87
+ """
88
+ start_time = time.time()
89
+ ctx = self.context_engine.gather()
90
+ if extra_context:
91
+ ctx.update(extra_context)
92
+ ctx.update(env_overrides)
93
+
94
+ result = ProcessResult(
95
+ process_name=self.process_config.process,
96
+ started_at=datetime.utcnow(),
97
+ )
98
+
99
+ for step in self.process_config.steps:
100
+ step_result = await self._execute_step(
101
+ step=step,
102
+ ctx=ctx,
103
+ approval_callback=approval_callback,
104
+ dry_run=dry_run,
105
+ )
106
+ result.steps.append(step_result)
107
+ self._step_results[step.name] = step_result
108
+
109
+ if step_result.status == StepStatus.FAILED:
110
+ logger.error(f"Step '{step.name}' failed — halting chain.")
111
+ break
112
+
113
+ if step_result.status == StepStatus.AWAITING_APPROVAL:
114
+ logger.info(f"Step '{step.name}' awaiting approval — chain paused.")
115
+ break
116
+
117
+ # Final status
118
+ all_completed = all(s.status == StepStatus.COMPLETED for s in result.steps)
119
+ result.completed = all_completed
120
+ result.total_duration_seconds = time.time() - start_time
121
+ result.finished_at = datetime.utcnow()
122
+
123
+ return result
124
+
125
+ async def _execute_step(
126
+ self,
127
+ step: ProcessStep,
128
+ ctx: dict[str, str],
129
+ approval_callback: ApprovalCallback | None,
130
+ dry_run: bool,
131
+ ) -> StepResult:
132
+ """Execute a single step in the chain."""
133
+ step_start = time.time()
134
+ step_result = StepResult(step_name=step.name, status=StepStatus.RUNNING)
135
+
136
+ # Check dependencies
137
+ for dep in step.depends_on:
138
+ dep_result = self._step_results.get(dep)
139
+ if not dep_result or dep_result.status != StepStatus.COMPLETED:
140
+ step_result.status = StepStatus.FAILED
141
+ step_result.error = f"Dependency '{dep}' not completed"
142
+ return step_result
143
+
144
+ # Enrich prompt with context
145
+ enriched_prompt = self.context_engine.enrich_prompt(step.prompt, ctx)
146
+
147
+ logger.info(f"Step '{step.name}': enriched prompt = {enriched_prompt[:100]}...")
148
+
149
+ # Approval gate
150
+ if step.approval == ApprovalMode.MANUAL:
151
+ if approval_callback:
152
+ try:
153
+ approved, approved_by = await approval_callback(step.name, enriched_prompt)
154
+ except Exception as e:
155
+ step_result.status = StepStatus.FAILED
156
+ step_result.error = f"Approval callback error: {e}"
157
+ return step_result
158
+
159
+ if not approved:
160
+ step_result.status = StepStatus.AWAITING_APPROVAL
161
+ return step_result
162
+
163
+ step_result.approved_by = approved_by
164
+ step_result.approved_at = datetime.utcnow()
165
+ step_result.status = StepStatus.APPROVED
166
+ else:
167
+ # No callback provided — pause for manual approval
168
+ step_result.status = StepStatus.AWAITING_APPROVAL
169
+ return step_result
170
+
171
+ # Dry run — just analyze, don't call LLM
172
+ if dry_run:
173
+ analysis = self.guard.analyze_only(enriched_prompt)
174
+ step_result.status = StepStatus.COMPLETED
175
+ step_result.duration_seconds = time.time() - step_start
176
+ logger.info(f"Step '{step.name}' (dry-run): {analysis}")
177
+ return step_result
178
+
179
+ # Execute through PromptGuard
180
+ try:
181
+ response = await self.guard(enriched_prompt, extra_context=ctx)
182
+ step_result.response = response
183
+ step_result.status = StepStatus.COMPLETED
184
+
185
+ self._audit_step(step.name, enriched_prompt, response.content)
186
+
187
+ except Exception as e:
188
+ step_result.status = StepStatus.FAILED
189
+ step_result.error = str(e)
190
+ logger.error(f"Step '{step.name}' failed: {e}")
191
+
192
+ if step.rollback:
193
+ logger.warning(f"Step '{step.name}' has rollback=true — rollback should be triggered")
194
+ step_result.status = StepStatus.ROLLED_BACK
195
+
196
+ step_result.duration_seconds = time.time() - step_start
197
+ return step_result
198
+
199
+ def get_audit_log(self) -> list[dict[str, Any]]:
200
+ return [e.model_dump() for e in self.audit_log]
201
+
202
+ def _audit_step(self, step_name: str, prompt: str, response: str) -> None:
203
+ entry = AuditEntry(
204
+ action="process_step",
205
+ query=prompt,
206
+ response_summary=response[:200] if response else "",
207
+ step_name=step_name,
208
+ process_name=self.process_config.process,
209
+ )
210
+ self.audit_log.append(entry)
211
+
212
+ @staticmethod
213
+ def _load_process_config(path: Path) -> ProcessConfig:
214
+ with open(path) as f:
215
+ raw = yaml.safe_load(f) or {}
216
+
217
+ steps = []
218
+ for s in raw.get("steps", []):
219
+ steps.append(ProcessStep(**s))
220
+
221
+ return ProcessConfig(
222
+ process=raw.get("process", "unnamed"),
223
+ description=raw.get("description", ""),
224
+ context_sources=raw.get("context_sources", []),
225
+ steps=steps,
226
+ )
@@ -0,0 +1,175 @@
1
+ """PromptGuard CLI — run prompts, execute process chains, and manage configs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ import sys
8
+ from pathlib import Path
9
+ from typing import Optional
10
+
11
+ import typer
12
+
13
+ app = typer.Typer(
14
+ name="promptguard",
15
+ help="PromptGuard — LLM prompt middleware for bias detection, standardization, and DevOps process chains.",
16
+ no_args_is_help=True,
17
+ )
18
+
19
+
20
+ @app.command()
21
+ def run(
22
+ query: str = typer.Argument(..., help="The prompt/query to process"),
23
+ config: Path = typer.Option("rules.yaml", "--config", "-c", help="Path to YAML config"),
24
+ model: str = typer.Option("gpt-4o-mini", "--model", "-m", help="LLM model to use"),
25
+ dry_run: bool = typer.Option(False, "--dry-run", "-d", help="Analyze only, don't call LLM"),
26
+ json_output: bool = typer.Option(False, "--json", "-j", help="Output as JSON"),
27
+ ):
28
+ """Run a single query through PromptGuard."""
29
+ from promptguard.core import PromptGuard
30
+
31
+ guard = PromptGuard(config_path=config)
32
+
33
+ if dry_run:
34
+ result = guard.analyze_only(query)
35
+ if json_output:
36
+ typer.echo(json.dumps(result, indent=2, default=str))
37
+ else:
38
+ typer.echo(f"🔍 Analysis for: {query}")
39
+ typer.echo(f" Needs clarification: {result['needs_clarify']}")
40
+ typer.echo(f" Patterns detected: {result['patterns']}")
41
+ typer.echo(f" Ambiguity flags: {result['ambiguity_flags']}")
42
+ typer.echo(f" Enriched query: {result['enriched']}")
43
+ return
44
+
45
+ result = asyncio.run(_run_guard(guard, query, model))
46
+
47
+ if json_output:
48
+ typer.echo(result.model_dump_json(indent=2))
49
+ else:
50
+ status = "✅ clarified" if result.clarified else "📝 direct"
51
+ typer.echo(f"\n{'='*60}")
52
+ typer.echo(f"🛡️ PromptGuard [{status}] via {result.model_used}")
53
+ typer.echo(f"{'='*60}")
54
+ typer.echo(f"\n{result.content}")
55
+ if result.analysis and result.analysis.detected_patterns:
56
+ typer.echo(f"\n⚠️ Detected: {', '.join(result.analysis.detected_patterns)}")
57
+ typer.echo(f"\n{'='*60}")
58
+
59
+
60
+ @app.command()
61
+ def process(
62
+ config: Path = typer.Argument(..., help="Path to process chain YAML"),
63
+ guard_config: Path = typer.Option("rules.yaml", "--guard-config", "-g", help="Path to guard YAML config"),
64
+ dry_run: bool = typer.Option(False, "--dry-run", "-d", help="Analyze steps without calling LLM"),
65
+ json_output: bool = typer.Option(False, "--json", "-j", help="Output as JSON"),
66
+ env: Optional[str] = typer.Option(None, "--env", "-e", help="Environment override (e.g., production)"),
67
+ ):
68
+ """Execute a DevOps process chain."""
69
+ from promptguard.chains.process_chain import ProcessChain
70
+
71
+ chain = ProcessChain(config_path=config, guard_config_path=guard_config)
72
+
73
+ extra = {}
74
+ if env:
75
+ extra["env"] = env
76
+
77
+ result = asyncio.run(chain.execute(extra_context=extra, dry_run=dry_run))
78
+
79
+ if json_output:
80
+ typer.echo(result.model_dump_json(indent=2))
81
+ else:
82
+ typer.echo(f"\n{'='*60}")
83
+ typer.echo(f"🔗 Process: {result.process_name}")
84
+ typer.echo(f" Status: {'✅ Completed' if result.completed else '⏸️ Incomplete'}")
85
+ typer.echo(f" Duration: {result.total_duration_seconds:.2f}s")
86
+ typer.echo(f"{'='*60}")
87
+ for step in result.steps:
88
+ icon = {
89
+ "completed": "✅",
90
+ "failed": "❌",
91
+ "awaiting_approval": "⏳",
92
+ "rolled_back": "↩️",
93
+ }.get(step.status.value, "🔄")
94
+ typer.echo(f" {icon} {step.step_name}: {step.status.value} ({step.duration_seconds:.2f}s)")
95
+ if step.error:
96
+ typer.echo(f" Error: {step.error}")
97
+ typer.echo(f"{'='*60}")
98
+
99
+
100
+ @app.command()
101
+ def analyze(
102
+ query: str = typer.Argument(..., help="Query to analyze for bias/ambiguity"),
103
+ config: Path = typer.Option("rules.yaml", "--config", "-c", help="Path to YAML config"),
104
+ ):
105
+ """Analyze a query without calling any LLM (bias detection + ambiguity check)."""
106
+ from promptguard.core import PromptGuard
107
+
108
+ guard = PromptGuard(config_path=config)
109
+ result = guard.analyze_only(query)
110
+
111
+ typer.echo(f"\n🔍 Analysis: {query}")
112
+ typer.echo(f" Needs clarification: {'⚠️ YES' if result['needs_clarify'] else '✅ NO'}")
113
+ if result["patterns"]:
114
+ typer.echo(f" Patterns: {', '.join(result['patterns'])}")
115
+ if result["ambiguity_flags"]:
116
+ typer.echo(f" Flags: {', '.join(result['ambiguity_flags'])}")
117
+ if result["readability"] is not None:
118
+ typer.echo(f" Readability: {result['readability']:.1f}")
119
+ typer.echo(f" Enriched: {result['enriched']}")
120
+
121
+
122
+ @app.command()
123
+ def init(
124
+ output: Path = typer.Option("rules.yaml", "--output", "-o", help="Output path for config"),
125
+ devops: bool = typer.Option(False, "--devops", help="Include DevOps-specific patterns"),
126
+ ):
127
+ """Generate a starter rules.yaml config file."""
128
+ import yaml
129
+
130
+ config = {
131
+ "bias_patterns": [
132
+ {"regex": "(zawsze|always)\\s+\\w+", "action": "clarify", "severity": "medium",
133
+ "description": "Absolute quantifier"},
134
+ {"regex": "(tylko|only|just)\\s+\\w+", "action": "clarify", "severity": "low",
135
+ "description": "Exclusive quantifier"},
136
+ ],
137
+ "clarify_template": "[KONTEKST]: Podaj szczegóły lub alternatywy dla: {query}",
138
+ "max_retries": 3,
139
+ "policy": "strict",
140
+ "models": {
141
+ "fallback": ["gpt-4o-mini", "llama3"],
142
+ "timeout": 30,
143
+ "max_tokens": 2048,
144
+ },
145
+ }
146
+
147
+ if devops:
148
+ config["bias_patterns"].extend([
149
+ {"regex": "(deploy|zdeployuj)\\s+(na|to)\\s+(prod|production)", "action": "clarify",
150
+ "severity": "critical", "description": "Production deployment without context"},
151
+ {"regex": "(delete|drop|remove|usuń)\\s+(database|db|baz)", "action": "clarify",
152
+ "severity": "critical", "description": "Destructive DB operation"},
153
+ {"regex": "(restart|reboot|kill)\\s+(server|service|pod)", "action": "clarify",
154
+ "severity": "high", "description": "Service disruption"},
155
+ {"regex": "(scale|skaluj)\\s+(down|up|to\\s+\\d+)", "action": "clarify",
156
+ "severity": "high", "description": "Scaling operation"},
157
+ ])
158
+ config["context_sources"] = [
159
+ {"env": ["CLUSTER", "NAMESPACE", "GIT_SHA", "ENV"]},
160
+ {"git": ["branch", "short_sha", "last_commit_msg"]},
161
+ {"system": ["hostname", "os"]},
162
+ ]
163
+
164
+ with open(output, "w") as f:
165
+ yaml.dump(config, f, default_flow_style=False, allow_unicode=True, sort_keys=False)
166
+
167
+ typer.echo(f"✅ Config written to {output}")
168
+
169
+
170
+ async def _run_guard(guard, query: str, model: str):
171
+ return await guard(query, model=model)
172
+
173
+
174
+ if __name__ == "__main__":
175
+ app()
@@ -0,0 +1,171 @@
1
+ """Core PromptGuard — the main entry point for prompt analysis, enrichment, and LLM calls."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from datetime import datetime
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ import yaml
11
+
12
+ from promptguard.analyzers.bias_detector import BiasDetector
13
+ from promptguard.analyzers.context_engine import ContextEngine
14
+ from promptguard.models import (
15
+ AuditEntry,
16
+ BiasPattern,
17
+ GuardConfig,
18
+ GuardResponse,
19
+ ModelConfig,
20
+ Policy,
21
+ )
22
+
23
+ logger = logging.getLogger("promptguard")
24
+
25
+
26
+ class PromptGuard:
27
+ """Main PromptGuard middleware — analyze, enrich, and proxy LLM calls.
28
+
29
+ Usage:
30
+ guard = PromptGuard("rules.yaml")
31
+ result = await guard("Zdeployuj na produkcję", model="gpt-4o-mini")
32
+
33
+ Or with inline config:
34
+ guard = PromptGuard(config=GuardConfig(policy=Policy.DEVOPS))
35
+ result = await guard("Deploy the app")
36
+ """
37
+
38
+ def __init__(
39
+ self,
40
+ config_path: str | Path | None = None,
41
+ config: GuardConfig | None = None,
42
+ ):
43
+ if config:
44
+ self.config = config
45
+ elif config_path:
46
+ self.config = self._load_config(Path(config_path))
47
+ else:
48
+ self.config = GuardConfig()
49
+
50
+ self.detector = BiasDetector(self.config.bias_patterns or None)
51
+ self.context_engine = ContextEngine(self.config.context_sources)
52
+ self.audit_log: list[AuditEntry] = []
53
+
54
+ async def __call__(
55
+ self,
56
+ query: str,
57
+ model: str | None = None,
58
+ extra_context: dict[str, str] | None = None,
59
+ **kwargs: Any,
60
+ ) -> GuardResponse:
61
+ """Analyze query, enrich if needed, call LLM, validate response."""
62
+ import litellm
63
+
64
+ target_model = model or self.config.models.fallback[0]
65
+
66
+ # Step 1: Analyze
67
+ analysis = self.detector.analyze(query)
68
+ analysis.original_query = query
69
+
70
+ # Step 2: Enrich if needed
71
+ if analysis.needs_clarify:
72
+ enriched = self.config.clarify_template.format(query=query)
73
+ enriched = self.context_engine.enrich_prompt(enriched, extra_context)
74
+ analysis.enriched_query = enriched
75
+ else:
76
+ enriched = self.context_engine.enrich_prompt(query, extra_context)
77
+ analysis.enriched_query = enriched
78
+
79
+ # Step 3: Call LLM with retry/fallback
80
+ response_content = ""
81
+ model_used = target_model
82
+ retries = 0
83
+
84
+ fallback_models = [target_model] + [
85
+ m for m in self.config.models.fallback if m != target_model
86
+ ]
87
+
88
+ for attempt_model in fallback_models:
89
+ for attempt in range(self.config.max_retries):
90
+ try:
91
+ resp = await litellm.acompletion(
92
+ model=attempt_model,
93
+ messages=[{"role": "user", "content": enriched}],
94
+ max_tokens=self.config.models.max_tokens,
95
+ timeout=self.config.models.timeout,
96
+ **kwargs,
97
+ )
98
+ response_content = resp.choices[0].message.content
99
+ model_used = attempt_model
100
+ break
101
+ except Exception as e:
102
+ retries += 1
103
+ logger.warning(f"Attempt {attempt + 1} with {attempt_model} failed: {e}")
104
+ if response_content:
105
+ break
106
+
107
+ # Step 4: Build response
108
+ result = GuardResponse(
109
+ content=response_content or "No response from any model.",
110
+ clarified=analysis.needs_clarify,
111
+ needs_more_context=analysis.needs_clarify and not response_content,
112
+ model_used=model_used,
113
+ analysis=analysis,
114
+ retries=retries,
115
+ )
116
+
117
+ # Audit
118
+ self._audit("query", query, result, model_used)
119
+
120
+ return result
121
+
122
+ def analyze_only(self, query: str) -> dict[str, Any]:
123
+ """Run analysis without calling LLM — useful for dry-run / testing."""
124
+ analysis = self.detector.analyze(query)
125
+ return {
126
+ "needs_clarify": analysis.needs_clarify,
127
+ "patterns": analysis.detected_patterns,
128
+ "ambiguity_flags": analysis.ambiguity_flags,
129
+ "readability": analysis.readability_score,
130
+ "enriched": self.config.clarify_template.format(query=query)
131
+ if analysis.needs_clarify
132
+ else query,
133
+ }
134
+
135
+ def get_audit_log(self) -> list[dict[str, Any]]:
136
+ """Return audit log as list of dicts."""
137
+ return [entry.model_dump() for entry in self.audit_log]
138
+
139
+ def _audit(self, action: str, query: str, response: GuardResponse, model: str) -> None:
140
+ entry = AuditEntry(
141
+ action=action,
142
+ query=query,
143
+ response_summary=response.content[:200] if response.content else "",
144
+ model=model,
145
+ policy=self.config.policy,
146
+ )
147
+ self.audit_log.append(entry)
148
+
149
+ @staticmethod
150
+ def _load_config(path: Path) -> GuardConfig:
151
+ """Load config from YAML file."""
152
+ with open(path) as f:
153
+ raw = yaml.safe_load(f) or {}
154
+
155
+ # Normalize bias_patterns
156
+ patterns = []
157
+ for p in raw.get("bias_patterns", []):
158
+ if isinstance(p, dict):
159
+ patterns.append(BiasPattern(**p))
160
+
161
+ models_raw = raw.get("models", {})
162
+ models = ModelConfig(**models_raw) if isinstance(models_raw, dict) else ModelConfig()
163
+
164
+ return GuardConfig(
165
+ bias_patterns=patterns,
166
+ clarify_template=raw.get("clarify_template", GuardConfig.model_fields["clarify_template"].default),
167
+ max_retries=raw.get("max_retries", 3),
168
+ policy=Policy(raw.get("policy", "strict")),
169
+ models=models,
170
+ context_sources=raw.get("context_sources", []),
171
+ )
@@ -0,0 +1,135 @@
1
+ """Data models for PromptGuard — all inputs/outputs are Pydantic v2 validated."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import enum
6
+ from datetime import datetime
7
+ from typing import Any
8
+
9
+ from pydantic import BaseModel, Field
10
+
11
+
12
+ # --- Enums ---
13
+
14
+ class Policy(str, enum.Enum):
15
+ STRICT = "strict"
16
+ LENIENT = "lenient"
17
+ DEVOPS = "devops"
18
+
19
+
20
+ class ApprovalMode(str, enum.Enum):
21
+ AUTO = "auto"
22
+ MANUAL = "manual"
23
+
24
+
25
+ class StepStatus(str, enum.Enum):
26
+ PENDING = "pending"
27
+ RUNNING = "running"
28
+ AWAITING_APPROVAL = "awaiting_approval"
29
+ APPROVED = "approved"
30
+ COMPLETED = "completed"
31
+ FAILED = "failed"
32
+ ROLLED_BACK = "rolled_back"
33
+
34
+
35
+ # --- Config models ---
36
+
37
+ class BiasPattern(BaseModel):
38
+ regex: str
39
+ action: str = "clarify"
40
+ severity: str = "medium"
41
+ description: str = ""
42
+
43
+
44
+ class ModelConfig(BaseModel):
45
+ fallback: list[str] = Field(default_factory=lambda: ["gpt-4o-mini"])
46
+ timeout: int = 30
47
+ max_tokens: int = 2048
48
+
49
+
50
+ class GuardConfig(BaseModel):
51
+ """Top-level YAML config model."""
52
+ bias_patterns: list[BiasPattern] = Field(default_factory=list)
53
+ clarify_template: str = "[KONTEKST]: Podaj szczegóły lub alternatywy dla: {query}"
54
+ max_retries: int = 3
55
+ policy: Policy = Policy.STRICT
56
+ models: ModelConfig = Field(default_factory=ModelConfig)
57
+ context_sources: list[dict[str, Any]] = Field(default_factory=list)
58
+
59
+
60
+ # --- Process chain models ---
61
+
62
+ class ProcessStep(BaseModel):
63
+ name: str
64
+ prompt: str
65
+ policy: Policy = Policy.STRICT
66
+ approval: ApprovalMode = ApprovalMode.AUTO
67
+ rollback: bool = False
68
+ timeout: int = 300
69
+ depends_on: list[str] = Field(default_factory=list)
70
+
71
+
72
+ class ProcessConfig(BaseModel):
73
+ process: str
74
+ description: str = ""
75
+ context_sources: list[dict[str, Any]] = Field(default_factory=list)
76
+ steps: list[ProcessStep]
77
+
78
+
79
+ # --- Runtime models ---
80
+
81
+ class AnalysisResult(BaseModel):
82
+ """Result of query analysis — what was detected and what was done."""
83
+ needs_clarify: bool = False
84
+ detected_patterns: list[str] = Field(default_factory=list)
85
+ enriched_query: str = ""
86
+ original_query: str = ""
87
+ readability_score: float | None = None
88
+ ambiguity_flags: list[str] = Field(default_factory=list)
89
+
90
+
91
+ class GuardResponse(BaseModel):
92
+ """Response from PromptGuard — the final output after analysis and LLM call."""
93
+ content: str
94
+ clarified: bool = False
95
+ needs_more_context: bool = False
96
+ model_used: str = ""
97
+ analysis: AnalysisResult | None = None
98
+ retries: int = 0
99
+ timestamp: datetime = Field(default_factory=datetime.utcnow)
100
+
101
+
102
+ class StepResult(BaseModel):
103
+ """Result of a single process chain step."""
104
+ step_name: str
105
+ status: StepStatus = StepStatus.PENDING
106
+ response: GuardResponse | None = None
107
+ approved_by: str | None = None
108
+ approved_at: datetime | None = None
109
+ error: str | None = None
110
+ duration_seconds: float = 0.0
111
+
112
+
113
+ class ProcessResult(BaseModel):
114
+ """Result of a full process chain execution."""
115
+ process_name: str
116
+ steps: list[StepResult] = Field(default_factory=list)
117
+ completed: bool = False
118
+ total_duration_seconds: float = 0.0
119
+ started_at: datetime = Field(default_factory=datetime.utcnow)
120
+ finished_at: datetime | None = None
121
+
122
+
123
+ # --- Audit models ---
124
+
125
+ class AuditEntry(BaseModel):
126
+ """Single audit log entry for traceability."""
127
+ timestamp: datetime = Field(default_factory=datetime.utcnow)
128
+ action: str
129
+ query: str = ""
130
+ response_summary: str = ""
131
+ model: str = ""
132
+ policy: Policy = Policy.STRICT
133
+ step_name: str | None = None
134
+ process_name: str | None = None
135
+ metadata: dict[str, Any] = Field(default_factory=dict)
@@ -0,0 +1,48 @@
1
+ [tool.poetry]
2
+ name = "prellm"
3
+ version = "0.1.12"
4
+ description = "PromptGuard - Lightweight LLM prompt middleware for bias detection, standardization, and DevOps process chains via YAML config, supporting context injection and multi-provider orchestration."
5
+ authors = [
6
+ "Softreck <tom@sapletta.com>",
7
+ "Tom Sapletta <tom@sapletta.com>",
8
+ ]
9
+ license = "Apache-2.0"
10
+ readme = "README.md"
11
+ repository = "https://github.com/softreck/gllm"
12
+ keywords = ["llm", "prompt-engineering", "bias-detection", "devops", "litellm"]
13
+ packages = [{include = "promptguard"}]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: Apache Software License",
18
+ ]
19
+
20
+ [tool.poetry.dependencies]
21
+ python = "^3.10"
22
+ litellm = "^1.40"
23
+ pydantic = "^2.0"
24
+ pyyaml = "^6.0"
25
+ nltk = "^3.8"
26
+ typer = {version = "^0.12", extras = ["all"]}
27
+ textstat = "^0.7"
28
+
29
+ [tool.poetry.group.dev.dependencies]
30
+ pytest = "^8.0"
31
+ pytest-asyncio = "^0.23"
32
+ pytest-mock = "^3.12"
33
+ ruff = "^0.4"
34
+
35
+ [tool.poetry.scripts]
36
+ promptguard = "promptguard.cli:app"
37
+
38
+ [build-system]
39
+ requires = ["poetry-core"]
40
+ build-backend = "poetry.core.masonry.api"
41
+
42
+ [tool.ruff]
43
+ target-version = "py310"
44
+ line-length = 120
45
+
46
+ [tool.pytest.ini_options]
47
+ asyncio_mode = "auto"
48
+ testpaths = ["tests"]