pgate 0.1.1__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.
- pgate-0.1.1/.github/workflows/publish.yml +49 -0
- pgate-0.1.1/.gitignore +10 -0
- pgate-0.1.1/PKG-INFO +58 -0
- pgate-0.1.1/README.md +34 -0
- pgate-0.1.1/examples/basic_usage.py +70 -0
- pgate-0.1.1/examples/report_sales.yaml +20 -0
- pgate-0.1.1/promptgate/__init__.py +6 -0
- pgate-0.1.1/promptgate/backends/__init__.py +3 -0
- pgate-0.1.1/promptgate/backends/base.py +15 -0
- pgate-0.1.1/promptgate/cli.py +365 -0
- pgate-0.1.1/promptgate/compiler.py +195 -0
- pgate-0.1.1/promptgate/file_api.py +143 -0
- pgate-0.1.1/promptgate/mcp_adapter.py +99 -0
- pgate-0.1.1/promptgate/models.py +75 -0
- pgate-0.1.1/promptgate/models_registry.py +293 -0
- pgate-0.1.1/promptgate/router.py +81 -0
- pgate-0.1.1/promptgate/storage.py +247 -0
- pgate-0.1.1/promptgate/validator.py +103 -0
- pgate-0.1.1/pyproject.toml +46 -0
- pgate-0.1.1/tests/__init__.py +0 -0
- pgate-0.1.1/tests/test_cli.py +112 -0
- pgate-0.1.1/tests/test_compiler.py +183 -0
- pgate-0.1.1/tests/test_file_api.py +101 -0
- pgate-0.1.1/tests/test_models_registry.py +276 -0
- pgate-0.1.1/tests/test_router.py +141 -0
- pgate-0.1.1/tests/test_storage.py +165 -0
- pgate-0.1.1/tests/test_validator.py +111 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
id-token: write # required for Trusted Publishing (OIDC)
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
build:
|
|
13
|
+
name: Build distribution
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- uses: actions/setup-python@v5
|
|
19
|
+
with:
|
|
20
|
+
python-version: "3.12"
|
|
21
|
+
|
|
22
|
+
- name: Install hatchling
|
|
23
|
+
run: pip install hatchling
|
|
24
|
+
|
|
25
|
+
- name: Build wheel and sdist
|
|
26
|
+
run: python -m hatchling build
|
|
27
|
+
|
|
28
|
+
- name: Upload dist artifacts
|
|
29
|
+
uses: actions/upload-artifact@v4
|
|
30
|
+
with:
|
|
31
|
+
name: dist
|
|
32
|
+
path: dist/
|
|
33
|
+
|
|
34
|
+
publish:
|
|
35
|
+
name: Publish to PyPI
|
|
36
|
+
needs: build
|
|
37
|
+
runs-on: ubuntu-latest
|
|
38
|
+
environment:
|
|
39
|
+
name: pypi
|
|
40
|
+
url: https://pypi.org/p/pgate
|
|
41
|
+
steps:
|
|
42
|
+
- name: Download dist artifacts
|
|
43
|
+
uses: actions/download-artifact@v4
|
|
44
|
+
with:
|
|
45
|
+
name: dist
|
|
46
|
+
path: dist/
|
|
47
|
+
|
|
48
|
+
- name: Publish to PyPI
|
|
49
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
pgate-0.1.1/.gitignore
ADDED
pgate-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pgate
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: PGate — Prompt ORM for LLM agents. Store, compile, validate and cache prompt contracts locally.
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Requires-Dist: click>=8.1
|
|
8
|
+
Requires-Dist: jinja2>=3.1
|
|
9
|
+
Requires-Dist: jsonschema>=4.0
|
|
10
|
+
Requires-Dist: loguru>=0.7
|
|
11
|
+
Requires-Dist: mcp>=1.0
|
|
12
|
+
Requires-Dist: pydantic>=2.0
|
|
13
|
+
Requires-Dist: pyyaml>=6.0
|
|
14
|
+
Provides-Extra: crypto
|
|
15
|
+
Requires-Dist: cryptography>=41.0; extra == 'crypto'
|
|
16
|
+
Provides-Extra: dev
|
|
17
|
+
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
18
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
19
|
+
Provides-Extra: litellm
|
|
20
|
+
Requires-Dist: litellm>=1.0; extra == 'litellm'
|
|
21
|
+
Provides-Extra: postgres
|
|
22
|
+
Requires-Dist: psycopg2-binary>=2.9; extra == 'postgres'
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# PGate
|
|
26
|
+
|
|
27
|
+
**Prompt ORM for LLM agents.** Store, find, compile, validate, and cache prompt contracts — locally, without cloud, without telemetry.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install pgate
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Quickstart
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pgate init
|
|
39
|
+
pgate add --file examples/report_sales.yaml
|
|
40
|
+
pgate compile --interactive
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## MCP (Claude Desktop / Cursor)
|
|
44
|
+
|
|
45
|
+
Add to `claude_desktop_config.json`:
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"pgate": {
|
|
50
|
+
"command": "pgate",
|
|
51
|
+
"args": ["mcp", "--stdio"]
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Status
|
|
57
|
+
|
|
58
|
+
v0.1 — in development.
|
pgate-0.1.1/README.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# PGate
|
|
2
|
+
|
|
3
|
+
**Prompt ORM for LLM agents.** Store, find, compile, validate, and cache prompt contracts — locally, without cloud, without telemetry.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install pgate
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quickstart
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pgate init
|
|
15
|
+
pgate add --file examples/report_sales.yaml
|
|
16
|
+
pgate compile --interactive
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## MCP (Claude Desktop / Cursor)
|
|
20
|
+
|
|
21
|
+
Add to `claude_desktop_config.json`:
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"pgate": {
|
|
26
|
+
"command": "pgate",
|
|
27
|
+
"args": ["mcp", "--stdio"]
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Status
|
|
33
|
+
|
|
34
|
+
v0.1 — in development.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Basic usage example for PromptGate v0.1."""
|
|
2
|
+
import tempfile
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from promptgate.models import PromptConfig
|
|
6
|
+
from promptgate.storage import SQLiteBackend
|
|
7
|
+
from promptgate.router import search
|
|
8
|
+
from promptgate.compiler import compile_prompt
|
|
9
|
+
from promptgate.validator import extract_json, validate_output
|
|
10
|
+
|
|
11
|
+
# Use temp DB for this demo
|
|
12
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
13
|
+
db_path = Path(tmpdir) / "demo.db"
|
|
14
|
+
|
|
15
|
+
# 1. Initialize storage
|
|
16
|
+
backend = SQLiteBackend(db_path)
|
|
17
|
+
backend.init()
|
|
18
|
+
|
|
19
|
+
# 2. Define and store a prompt
|
|
20
|
+
prompt = PromptConfig(
|
|
21
|
+
id="sales_report_v1",
|
|
22
|
+
name="Sales Report Generator",
|
|
23
|
+
tags=["sales", "reporting", "finance"],
|
|
24
|
+
**{"schema": {
|
|
25
|
+
"type": "object",
|
|
26
|
+
"properties": {
|
|
27
|
+
"period": {"type": "string", "pattern": r"^\d{4}-\d{2}$"},
|
|
28
|
+
"region": {"type": "string"},
|
|
29
|
+
},
|
|
30
|
+
"required": ["period"],
|
|
31
|
+
}},
|
|
32
|
+
template=(
|
|
33
|
+
"You are a sales analyst. Generate a concise report for period {{ period }}"
|
|
34
|
+
"{% if region %} in region {{ region }}{% endif %}.\n"
|
|
35
|
+
"Payload: {{ payload_json }}\n"
|
|
36
|
+
"Return JSON matching: {{ output_schema }}"
|
|
37
|
+
),
|
|
38
|
+
description="Monthly sales report with optional region filter",
|
|
39
|
+
)
|
|
40
|
+
backend.upsert(prompt)
|
|
41
|
+
print(f"Stored prompt: {prompt.id}")
|
|
42
|
+
|
|
43
|
+
# 3. Search
|
|
44
|
+
results = search("sales report", db_path=db_path)
|
|
45
|
+
print(f"Search 'sales report': {results}")
|
|
46
|
+
|
|
47
|
+
# 4. Compile
|
|
48
|
+
contract = compile_prompt(prompt, {"period": "2024-01", "region": "EMEA"})
|
|
49
|
+
print(f"\nCompiled contract:")
|
|
50
|
+
print(f" schema_hash: {contract.schema_hash}")
|
|
51
|
+
print(f" missing_fields: {contract.missing_fields}")
|
|
52
|
+
print(f" system_prompt:\n{contract.system_prompt[:200]}...")
|
|
53
|
+
|
|
54
|
+
# 5. Validate LLM output (simulated)
|
|
55
|
+
simulated_llm_output = """
|
|
56
|
+
Sure, here is the report:
|
|
57
|
+
{"summary": "Q1 EMEA sales up 12%", "period": "2024-01", "region": "EMEA"}
|
|
58
|
+
"""
|
|
59
|
+
result = validate_output(
|
|
60
|
+
simulated_llm_output,
|
|
61
|
+
contract,
|
|
62
|
+
schema={"type": "object", "properties": {"summary": {"type": "string"}}},
|
|
63
|
+
)
|
|
64
|
+
print(f"\nValidation: ok={result.ok}, data={result.data}")
|
|
65
|
+
|
|
66
|
+
# 6. Demonstrate missing fields handling
|
|
67
|
+
contract_missing = compile_prompt(prompt, {})
|
|
68
|
+
print(f"\nMissing fields test: {contract_missing.missing_fields}")
|
|
69
|
+
|
|
70
|
+
print("\nDone.")
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
id: "report_sales_v2"
|
|
2
|
+
name: "Генерация отчёта по продажам"
|
|
3
|
+
tags: ["report", "finance", "stable"]
|
|
4
|
+
schema:
|
|
5
|
+
type: object
|
|
6
|
+
required: ["period", "metrics"]
|
|
7
|
+
properties:
|
|
8
|
+
period:
|
|
9
|
+
type: string
|
|
10
|
+
pattern: "^\\d{4}-\\d{2}$"
|
|
11
|
+
metrics:
|
|
12
|
+
type: array
|
|
13
|
+
items:
|
|
14
|
+
type: string
|
|
15
|
+
enum: ["revenue", "orders", "aov"]
|
|
16
|
+
template: |
|
|
17
|
+
Ты — аналитик данных. Входные данные (валидированы):
|
|
18
|
+
{{ payload_json }}
|
|
19
|
+
Верни строго по схеме: {{ output_schema }}
|
|
20
|
+
Если данных нет — используй поле "missing_info".
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Protocol, runtime_checkable
|
|
4
|
+
|
|
5
|
+
from promptgate.models import PromptConfig
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@runtime_checkable
|
|
9
|
+
class StorageBackend(Protocol):
|
|
10
|
+
def init(self) -> None: ...
|
|
11
|
+
def upsert(self, prompt: PromptConfig) -> None: ...
|
|
12
|
+
def get(self, prompt_id: str) -> PromptConfig | None: ...
|
|
13
|
+
def delete(self, prompt_id: str) -> bool: ...
|
|
14
|
+
def list_all(self) -> list[PromptConfig]: ...
|
|
15
|
+
def search_fts(self, query: str, limit: int = 5) -> list[tuple[str, float]]: ...
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
"""Click CLI: init, add, search, compile, mcp."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
import yaml
|
|
10
|
+
from loguru import logger
|
|
11
|
+
|
|
12
|
+
from promptgate.file_api import get_or_compile, purge_stale
|
|
13
|
+
from promptgate.models import PromptConfig
|
|
14
|
+
from promptgate.router import search as pg_search
|
|
15
|
+
from promptgate.storage import SQLiteBackend, _DEFAULT_DB_PATH
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _configure_logging(verbose: bool) -> None:
|
|
19
|
+
logger.remove()
|
|
20
|
+
level = "DEBUG" if verbose else "INFO"
|
|
21
|
+
logger.add(sys.stderr, level=level, format="<level>{level}</level> {message}")
|
|
22
|
+
log_dir = Path("logs")
|
|
23
|
+
log_dir.mkdir(exist_ok=True)
|
|
24
|
+
logger.add(
|
|
25
|
+
log_dir / "app.log",
|
|
26
|
+
level="DEBUG",
|
|
27
|
+
rotation="10 MB",
|
|
28
|
+
retention="7 days",
|
|
29
|
+
encoding="utf-8",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@click.group()
|
|
34
|
+
@click.option("--verbose", "-v", is_flag=True, default=False, help="Enable debug logging.")
|
|
35
|
+
@click.option(
|
|
36
|
+
"--db",
|
|
37
|
+
default=str(_DEFAULT_DB_PATH),
|
|
38
|
+
show_default=True,
|
|
39
|
+
help="Path to SQLite database.",
|
|
40
|
+
)
|
|
41
|
+
@click.pass_context
|
|
42
|
+
def main(ctx: click.Context, verbose: bool, db: str) -> None:
|
|
43
|
+
"""PromptGate - Prompt ORM for LLM agents."""
|
|
44
|
+
ctx.ensure_object(dict)
|
|
45
|
+
ctx.obj["db"] = db
|
|
46
|
+
_configure_logging(verbose)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@main.command()
|
|
50
|
+
@click.pass_context
|
|
51
|
+
def init(ctx: click.Context) -> None:
|
|
52
|
+
"""Initialize the PromptGate database.
|
|
53
|
+
|
|
54
|
+
Creates ~/.promptgate/db.sqlite and FTS5 tables if they don't exist.
|
|
55
|
+
"""
|
|
56
|
+
backend = SQLiteBackend(ctx.obj["db"])
|
|
57
|
+
backend.init()
|
|
58
|
+
click.echo(f"Initialized: {ctx.obj['db']}")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@main.command("add")
|
|
62
|
+
@click.option("--file", "-f", "file_path", type=click.Path(exists=True), help="Path to YAML prompt file.")
|
|
63
|
+
@click.option("--url", "-u", help="URL to fetch YAML prompt from.")
|
|
64
|
+
@click.pass_context
|
|
65
|
+
def add_prompt(ctx: click.Context, file_path: str | None, url: str | None) -> None:
|
|
66
|
+
"""Add or update a prompt from a YAML file or URL.
|
|
67
|
+
|
|
68
|
+
Examples:
|
|
69
|
+
|
|
70
|
+
promptgate add --file examples/report_sales.yaml
|
|
71
|
+
|
|
72
|
+
promptgate add --url https://example.com/my_prompt.yaml
|
|
73
|
+
"""
|
|
74
|
+
if not file_path and not url:
|
|
75
|
+
raise click.UsageError("Provide --file or --url.")
|
|
76
|
+
|
|
77
|
+
raw: str
|
|
78
|
+
if url:
|
|
79
|
+
import urllib.request
|
|
80
|
+
with urllib.request.urlopen(url) as resp: # noqa: S310
|
|
81
|
+
raw = resp.read().decode("utf-8")
|
|
82
|
+
else:
|
|
83
|
+
raw = Path(file_path).read_text(encoding="utf-8")
|
|
84
|
+
|
|
85
|
+
data = yaml.safe_load(raw)
|
|
86
|
+
prompt = PromptConfig.model_validate(data)
|
|
87
|
+
|
|
88
|
+
backend = SQLiteBackend(ctx.obj["db"])
|
|
89
|
+
backend.init()
|
|
90
|
+
backend.upsert(prompt)
|
|
91
|
+
click.echo(f"Added prompt: {prompt.id} ({prompt.name})")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@main.command("search")
|
|
95
|
+
@click.argument("query")
|
|
96
|
+
@click.option("--limit", "-n", default=5, show_default=True, help="Max results.")
|
|
97
|
+
@click.pass_context
|
|
98
|
+
def search_cmd(ctx: click.Context, query: str, limit: int) -> None:
|
|
99
|
+
"""Search prompts by full-text relevance.
|
|
100
|
+
|
|
101
|
+
Examples:
|
|
102
|
+
|
|
103
|
+
promptgate search "sales report"
|
|
104
|
+
"""
|
|
105
|
+
results = pg_search(query, db_path=ctx.obj["db"], limit=limit)
|
|
106
|
+
if not results:
|
|
107
|
+
click.echo("No results.")
|
|
108
|
+
return
|
|
109
|
+
for prompt_id, score in results:
|
|
110
|
+
click.echo(f" {score:.4f} {prompt_id}")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@main.command("compile")
|
|
114
|
+
@click.argument("prompt_id")
|
|
115
|
+
@click.option("--payload", "-p", default="{}", help="JSON payload string.")
|
|
116
|
+
@click.option("--model", "-m", default=None, help="LLM model ID for model-aware compilation.")
|
|
117
|
+
@click.option("--interactive", "-i", is_flag=True, help="Prompt for each required field interactively.")
|
|
118
|
+
@click.pass_context
|
|
119
|
+
def compile_cmd(ctx: click.Context, prompt_id: str, payload: str, model: str | None, interactive: bool) -> None:
|
|
120
|
+
"""Compile a prompt and print the system_prompt.
|
|
121
|
+
|
|
122
|
+
Examples:
|
|
123
|
+
|
|
124
|
+
promptgate compile sales_v1 --payload '{"period": "2024-01"}'
|
|
125
|
+
|
|
126
|
+
promptgate compile sales_v1 --model gpt-4o --payload '{"period": "2024-01"}'
|
|
127
|
+
|
|
128
|
+
promptgate compile sales_v1 --interactive
|
|
129
|
+
"""
|
|
130
|
+
backend = SQLiteBackend(ctx.obj["db"])
|
|
131
|
+
backend.init()
|
|
132
|
+
prompt = backend.get(prompt_id)
|
|
133
|
+
if prompt is None:
|
|
134
|
+
raise click.ClickException(f"Prompt '{prompt_id}' not found.")
|
|
135
|
+
|
|
136
|
+
data: dict = json.loads(payload)
|
|
137
|
+
|
|
138
|
+
if interactive:
|
|
139
|
+
required = prompt.schema_.get("required", [])
|
|
140
|
+
props = prompt.schema_.get("properties", {})
|
|
141
|
+
for field in required:
|
|
142
|
+
if field not in data:
|
|
143
|
+
desc = props.get(field, {})
|
|
144
|
+
hint = desc.get("type", "string")
|
|
145
|
+
value = click.prompt(f" {field} ({hint})")
|
|
146
|
+
data[field] = value
|
|
147
|
+
|
|
148
|
+
contract = get_or_compile(prompt_id, data, db_path=ctx.obj["db"], model_id=model)
|
|
149
|
+
click.echo(contract.system_prompt)
|
|
150
|
+
if contract.missing_fields:
|
|
151
|
+
click.echo(f"\n[missing: {', '.join(contract.missing_fields)}]", err=True)
|
|
152
|
+
if contract.model_hints:
|
|
153
|
+
h = contract.model_hints
|
|
154
|
+
rm_tag = {"none": "", "chain_of_thought": f" [CoT:{h.cot_strategy}]", "native": " [think]"}.get(
|
|
155
|
+
h.reasoning_mode, ""
|
|
156
|
+
)
|
|
157
|
+
sys_tag = "" if h.supports_system_prompt else " [no-sys]"
|
|
158
|
+
logger.debug("Model: {} ({}) ctx={}k{}{}", h.model_id, h.family, h.context_window // 1000, rm_tag, sys_tag)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@main.group("models", invoke_without_command=True)
|
|
162
|
+
@click.pass_context
|
|
163
|
+
def models_group(ctx: click.Context) -> None:
|
|
164
|
+
"""Manage LLM model registry.
|
|
165
|
+
|
|
166
|
+
Without a subcommand, lists all registered models.
|
|
167
|
+
|
|
168
|
+
Examples:
|
|
169
|
+
|
|
170
|
+
promptgate models
|
|
171
|
+
|
|
172
|
+
promptgate models list --family anthropic
|
|
173
|
+
|
|
174
|
+
promptgate models add --id gpt-5 --family openai --ctx 400000 --reasoning native
|
|
175
|
+
|
|
176
|
+
promptgate models rm gpt-5
|
|
177
|
+
|
|
178
|
+
promptgate models show gpt-4o
|
|
179
|
+
"""
|
|
180
|
+
if ctx.invoked_subcommand is None:
|
|
181
|
+
ctx.invoke(models_list)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _print_models_table(rows: list[tuple[str, object]], custom_ids: set[str]) -> None:
|
|
185
|
+
"""Print a formatted table row for each (model_id, ModelCapabilities) pair."""
|
|
186
|
+
from promptgate.models_registry import ModelCapabilities as MC
|
|
187
|
+
rm_labels = {"none": " ", "chain_of_thought": " CoT ", "native": "think"}
|
|
188
|
+
for name, caps in rows:
|
|
189
|
+
rm = rm_labels.get(caps.reasoning_mode, " ")
|
|
190
|
+
cot = f"/{caps.cot_strategy}" if caps.reasoning_mode == "chain_of_thought" else ""
|
|
191
|
+
sys_tag = " " if caps.supports_system_prompt else "NS"
|
|
192
|
+
json_tag = "J" if caps.json_native else " "
|
|
193
|
+
ctx_k = caps.context_window // 1000
|
|
194
|
+
custom_tag = "*" if name in custom_ids else " "
|
|
195
|
+
click.echo(f" {custom_tag}[{rm}{cot:<12}] [{sys_tag}][{json_tag}] ctx={ctx_k:>5}k {name}")
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@models_group.command("list")
|
|
199
|
+
@click.option("--family", "-f", default=None, help="Filter by family (openai, anthropic, deepseek...)")
|
|
200
|
+
@click.option("--reasoning", "-r", is_flag=True, help="Show only models with reasoning support.")
|
|
201
|
+
@click.option("--custom", "-c", is_flag=True, help="Show only user-defined models.")
|
|
202
|
+
def models_list(family: str | None, reasoning: bool, custom: bool) -> None:
|
|
203
|
+
"""List registered LLM models and their capabilities.
|
|
204
|
+
|
|
205
|
+
A leading ``*`` marks user-defined models.
|
|
206
|
+
|
|
207
|
+
Examples:
|
|
208
|
+
|
|
209
|
+
promptgate models list
|
|
210
|
+
|
|
211
|
+
promptgate models list --family anthropic
|
|
212
|
+
|
|
213
|
+
promptgate models list --reasoning
|
|
214
|
+
|
|
215
|
+
promptgate models list --custom
|
|
216
|
+
"""
|
|
217
|
+
from promptgate.models_registry import _REGISTRY, _USER_REGISTRY
|
|
218
|
+
|
|
219
|
+
all_rows: dict = {**_REGISTRY, **_USER_REGISTRY} # user overrides win
|
|
220
|
+
rows = sorted(all_rows.items())
|
|
221
|
+
if family:
|
|
222
|
+
rows = [(k, v) for k, v in rows if v.family == family]
|
|
223
|
+
if reasoning:
|
|
224
|
+
rows = [(k, v) for k, v in rows if v.reasoning_mode != "none"]
|
|
225
|
+
if custom:
|
|
226
|
+
rows = [(k, v) for k, v in rows if k in _USER_REGISTRY]
|
|
227
|
+
|
|
228
|
+
if not rows:
|
|
229
|
+
click.echo("No models match the filter.")
|
|
230
|
+
return
|
|
231
|
+
_print_models_table(rows, set(_USER_REGISTRY.keys()))
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@models_group.command("add")
|
|
235
|
+
@click.option("--id", "model_id", required=True, help="Unique model identifier.")
|
|
236
|
+
@click.option("--family", "-f", required=True, help="Provider family (openai, anthropic, deepseek...)")
|
|
237
|
+
@click.option("--ctx", "context_window", required=True, type=int, help="Context window in tokens.")
|
|
238
|
+
@click.option(
|
|
239
|
+
"--reasoning", "-r", default="none",
|
|
240
|
+
type=click.Choice(["none", "chain_of_thought", "native"]),
|
|
241
|
+
help="Reasoning mode.",
|
|
242
|
+
)
|
|
243
|
+
@click.option(
|
|
244
|
+
"--cot-strategy", default="zero_shot",
|
|
245
|
+
type=click.Choice(["zero_shot", "structured", "react"]),
|
|
246
|
+
help="CoT algorithm for chain_of_thought models.",
|
|
247
|
+
)
|
|
248
|
+
@click.option("--json-native", is_flag=True, help="Model supports native JSON output mode.")
|
|
249
|
+
@click.option("--no-system-prompt", is_flag=True, help="Model does not accept a system prompt.")
|
|
250
|
+
@click.option("--reasoning-budget", type=int, default=None, help="Token budget for native reasoning.")
|
|
251
|
+
@click.option("--max-output", type=int, default=4096, help="Max output tokens.")
|
|
252
|
+
def models_add(
|
|
253
|
+
model_id: str,
|
|
254
|
+
family: str,
|
|
255
|
+
context_window: int,
|
|
256
|
+
reasoning: str,
|
|
257
|
+
cot_strategy: str,
|
|
258
|
+
json_native: bool,
|
|
259
|
+
no_system_prompt: bool,
|
|
260
|
+
reasoning_budget: int | None,
|
|
261
|
+
max_output: int,
|
|
262
|
+
) -> None:
|
|
263
|
+
"""Add or override a model in the user registry.
|
|
264
|
+
|
|
265
|
+
Writes to ``~/.promptgate/models.yaml``.
|
|
266
|
+
|
|
267
|
+
Examples:
|
|
268
|
+
|
|
269
|
+
promptgate models add --id gpt-5 --family openai --ctx 400000 --reasoning native
|
|
270
|
+
|
|
271
|
+
promptgate models add --id my-llm --family custom --ctx 8000 --reasoning chain_of_thought --cot-strategy structured
|
|
272
|
+
"""
|
|
273
|
+
from promptgate.models_registry import ModelCapabilities, register_model
|
|
274
|
+
|
|
275
|
+
caps = ModelCapabilities(
|
|
276
|
+
family=family,
|
|
277
|
+
context_window=context_window,
|
|
278
|
+
reasoning_mode=reasoning,
|
|
279
|
+
cot_strategy=cot_strategy,
|
|
280
|
+
json_native=json_native,
|
|
281
|
+
supports_system_prompt=not no_system_prompt,
|
|
282
|
+
reasoning_budget_tokens=reasoning_budget,
|
|
283
|
+
max_output_tokens=max_output,
|
|
284
|
+
)
|
|
285
|
+
register_model(model_id, caps)
|
|
286
|
+
click.echo(f"Registered '{model_id}' ({family}, ctx={context_window:,}) → ~/.promptgate/models.yaml")
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
@models_group.command("rm")
|
|
290
|
+
@click.argument("model_id")
|
|
291
|
+
def models_rm(model_id: str) -> None:
|
|
292
|
+
"""Remove a custom model from the user registry.
|
|
293
|
+
|
|
294
|
+
Only user-defined models can be removed; built-in entries are protected.
|
|
295
|
+
|
|
296
|
+
Examples:
|
|
297
|
+
|
|
298
|
+
promptgate models rm my-custom-llm
|
|
299
|
+
"""
|
|
300
|
+
from promptgate.models_registry import unregister_model
|
|
301
|
+
|
|
302
|
+
if not unregister_model(model_id):
|
|
303
|
+
raise click.ClickException(
|
|
304
|
+
f"'{model_id}' not found in user registry. Built-in models cannot be removed."
|
|
305
|
+
)
|
|
306
|
+
click.echo(f"Removed '{model_id}' from ~/.promptgate/models.yaml")
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
@models_group.command("show")
|
|
310
|
+
@click.argument("model_id")
|
|
311
|
+
def models_show(model_id: str) -> None:
|
|
312
|
+
"""Show full capabilities for a specific model.
|
|
313
|
+
|
|
314
|
+
Examples:
|
|
315
|
+
|
|
316
|
+
promptgate models show gpt-4o
|
|
317
|
+
|
|
318
|
+
promptgate models show my-custom-llm
|
|
319
|
+
"""
|
|
320
|
+
from promptgate.models_registry import _USER_REGISTRY, resolve_model
|
|
321
|
+
|
|
322
|
+
caps = resolve_model(model_id)
|
|
323
|
+
if caps is None:
|
|
324
|
+
raise click.ClickException(f"Unknown model: '{model_id}'")
|
|
325
|
+
|
|
326
|
+
is_custom = model_id in _USER_REGISTRY
|
|
327
|
+
source = "user-defined" if is_custom else "built-in"
|
|
328
|
+
click.echo(f" model_id: {model_id} [{source}]")
|
|
329
|
+
click.echo(f" family: {caps.family}")
|
|
330
|
+
click.echo(f" context_window: {caps.context_window:,}")
|
|
331
|
+
click.echo(f" max_output_tokens: {caps.max_output_tokens:,}")
|
|
332
|
+
click.echo(f" reasoning_mode: {caps.reasoning_mode}")
|
|
333
|
+
click.echo(f" cot_strategy: {caps.cot_strategy}")
|
|
334
|
+
click.echo(f" json_native: {caps.json_native}")
|
|
335
|
+
click.echo(f" supports_system_prompt: {caps.supports_system_prompt}")
|
|
336
|
+
if caps.reasoning_budget_tokens is not None:
|
|
337
|
+
click.echo(f" reasoning_budget_tokens: {caps.reasoning_budget_tokens:,}")
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
@main.command("mcp")
|
|
341
|
+
@click.option("--stdio", "transport", flag_value="stdio", default=True, help="Run MCP over stdio.")
|
|
342
|
+
@click.pass_context
|
|
343
|
+
def mcp_cmd(ctx: click.Context, transport: str) -> None:
|
|
344
|
+
"""Start the PromptGate MCP server.
|
|
345
|
+
|
|
346
|
+
Add to Claude Desktop config:
|
|
347
|
+
|
|
348
|
+
{\"command\": \"promptgate\", \"args\": [\"mcp\", \"--stdio\"]}
|
|
349
|
+
"""
|
|
350
|
+
from promptgate.mcp_adapter import run_stdio
|
|
351
|
+
run_stdio(db_path=ctx.obj["db"])
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
@main.command("purge")
|
|
355
|
+
@click.option("--days", default=7, show_default=True, help="Delete contracts older than N days.")
|
|
356
|
+
@click.pass_context
|
|
357
|
+
def purge_cmd(ctx: click.Context, days: int) -> None:
|
|
358
|
+
"""Delete stale compiled contract files.
|
|
359
|
+
|
|
360
|
+
Examples:
|
|
361
|
+
|
|
362
|
+
promptgate purge --days 3
|
|
363
|
+
"""
|
|
364
|
+
deleted = purge_stale(max_age_seconds=days * 86400)
|
|
365
|
+
click.echo(f"Deleted {deleted} stale contract file(s).")
|