instructvault 0.2.2__tar.gz → 0.2.3__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.
- instructvault-0.2.3/.github/workflows/ci.yml +32 -0
- instructvault-0.2.3/.github/workflows/prompt-checks.yml +30 -0
- {instructvault-0.2.2 → instructvault-0.2.3}/PKG-INFO +20 -1
- {instructvault-0.2.2 → instructvault-0.2.3}/README.md +18 -0
- instructvault-0.2.3/docs/assets/playground.png +0 -0
- instructvault-0.2.3/docs/vision.md +76 -0
- instructvault-0.2.3/examples/prompts/hello_world.prompt.yml +14 -0
- {instructvault-0.2.2 → instructvault-0.2.3}/playground/README.md +15 -0
- instructvault-0.2.3/playground/ivault_playground/app.py +29 -0
- instructvault-0.2.3/playground/ivault_playground/routes/api.py +103 -0
- instructvault-0.2.3/playground/ivault_playground/routes/ui.py +12 -0
- instructvault-0.2.3/playground/ivault_playground/static/app.css +145 -0
- instructvault-0.2.3/playground/ivault_playground/static/app.js +116 -0
- instructvault-0.2.3/playground/ivault_playground/templates/index.html +70 -0
- {instructvault-0.2.2 → instructvault-0.2.3}/pyproject.toml +2 -1
- {instructvault-0.2.2 → instructvault-0.2.3}/src/instructvault/bundle.py +13 -4
- {instructvault-0.2.2 → instructvault-0.2.3}/src/instructvault/cli.py +9 -5
- {instructvault-0.2.2 → instructvault-0.2.3}/src/instructvault/io.py +6 -3
- {instructvault-0.2.2 → instructvault-0.2.3}/src/instructvault/sdk.py +3 -1
- {instructvault-0.2.2 → instructvault-0.2.3}/src/instructvault/spec.py +17 -1
- {instructvault-0.2.2 → instructvault-0.2.3}/tests/test_cli_basic.py +79 -1
- instructvault-0.2.3/tests/test_playground_api.py +80 -0
- instructvault-0.2.2/VISION_DOC.md +0 -426
- instructvault-0.2.2/playground/ivault_playground/app.py +0 -187
- {instructvault-0.2.2 → instructvault-0.2.3}/.github/CODEOWNERS +0 -0
- {instructvault-0.2.2 → instructvault-0.2.3}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {instructvault-0.2.2 → instructvault-0.2.3}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {instructvault-0.2.2 → instructvault-0.2.3}/.github/pull_request_template.md +0 -0
- {instructvault-0.2.2 → instructvault-0.2.3}/.github/workflows/release.yml +0 -0
- {instructvault-0.2.2 → instructvault-0.2.3}/CHANGELOG.md +0 -0
- {instructvault-0.2.2 → instructvault-0.2.3}/CODE_OF_CONDUCT.md +0 -0
- {instructvault-0.2.2 → instructvault-0.2.3}/CONTRIBUTING.md +0 -0
- {instructvault-0.2.2 → instructvault-0.2.3}/LICENSE +0 -0
- {instructvault-0.2.2 → instructvault-0.2.3}/SECURITY.md +0 -0
- {instructvault-0.2.2 → instructvault-0.2.3}/docs/ci.md +0 -0
- {instructvault-0.2.2 → instructvault-0.2.3}/docs/ci_templates/Jenkinsfile +0 -0
- {instructvault-0.2.2 → instructvault-0.2.3}/docs/ci_templates/gitlab-ci.yml +0 -0
- {instructvault-0.2.2 → instructvault-0.2.3}/docs/cookbooks.md +0 -0
- {instructvault-0.2.2 → instructvault-0.2.3}/docs/dropin_guide.md +0 -0
- {instructvault-0.2.2 → instructvault-0.2.3}/docs/governance.md +0 -0
- {instructvault-0.2.2 → instructvault-0.2.3}/docs/playground.md +0 -0
- {instructvault-0.2.2 → instructvault-0.2.3}/docs/release_checklist.md +0 -0
- {instructvault-0.2.2 → instructvault-0.2.3}/docs/templates/CODEOWNERS +0 -0
- {instructvault-0.2.2 → instructvault-0.2.3}/examples/datasets/classifier_cases.jsonl +0 -0
- {instructvault-0.2.2 → instructvault-0.2.3}/examples/datasets/rag_agent_cases.jsonl +0 -0
- {instructvault-0.2.2 → instructvault-0.2.3}/examples/datasets/rag_answer_cases.jsonl +0 -0
- {instructvault-0.2.2 → instructvault-0.2.3}/examples/datasets/support_cases.jsonl +0 -0
- {instructvault-0.2.2 → instructvault-0.2.3}/examples/prompts/classifier.prompt.yml +0 -0
- {instructvault-0.2.2 → instructvault-0.2.3}/examples/prompts/guardrail.prompt.json +0 -0
- {instructvault-0.2.2 → instructvault-0.2.3}/examples/prompts/rag_agent.prompt.yml +0 -0
- {instructvault-0.2.2 → instructvault-0.2.3}/examples/prompts/rag_answer.prompt.yml +0 -0
- {instructvault-0.2.2 → instructvault-0.2.3}/examples/prompts/support_reply.prompt.yml +0 -0
- {instructvault-0.2.2 → instructvault-0.2.3}/playground/ivault_playground/__init__.py +0 -0
- {instructvault-0.2.2 → instructvault-0.2.3}/playground/pyproject.toml +0 -0
- {instructvault-0.2.2 → instructvault-0.2.3}/src/instructvault/__init__.py +0 -0
- {instructvault-0.2.2 → instructvault-0.2.3}/src/instructvault/diff.py +0 -0
- {instructvault-0.2.2 → instructvault-0.2.3}/src/instructvault/eval.py +0 -0
- {instructvault-0.2.2 → instructvault-0.2.3}/src/instructvault/junit.py +0 -0
- {instructvault-0.2.2 → instructvault-0.2.3}/src/instructvault/render.py +0 -0
- {instructvault-0.2.2 → instructvault-0.2.3}/src/instructvault/scaffold.py +0 -0
- {instructvault-0.2.2 → instructvault-0.2.3}/src/instructvault/store.py +0 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
name: ci
|
|
2
|
+
on:
|
|
3
|
+
pull_request:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ main ]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
core-tests:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v4
|
|
12
|
+
- uses: actions/setup-python@v5
|
|
13
|
+
with:
|
|
14
|
+
python-version: "3.11"
|
|
15
|
+
- run: pip install -e ".[dev]"
|
|
16
|
+
- run: pytest
|
|
17
|
+
|
|
18
|
+
prompt-checks:
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v4
|
|
22
|
+
- uses: actions/setup-python@v5
|
|
23
|
+
with:
|
|
24
|
+
python-version: "3.11"
|
|
25
|
+
- run: pip install instructvault
|
|
26
|
+
- run: ivault validate prompts
|
|
27
|
+
- run: ivault eval prompts/hello_world.prompt.yml --report out/report.json --junit out/junit.xml
|
|
28
|
+
- uses: actions/upload-artifact@v4
|
|
29
|
+
if: always()
|
|
30
|
+
with:
|
|
31
|
+
name: ivault-reports
|
|
32
|
+
path: out/
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
name: prompt-checks
|
|
2
|
+
on:
|
|
3
|
+
pull_request:
|
|
4
|
+
paths:
|
|
5
|
+
- "prompts/**"
|
|
6
|
+
- "datasets/**"
|
|
7
|
+
- ".github/workflows/prompt-checks.yml"
|
|
8
|
+
push:
|
|
9
|
+
branches: [ main ]
|
|
10
|
+
paths:
|
|
11
|
+
- "prompts/**"
|
|
12
|
+
- "datasets/**"
|
|
13
|
+
- ".github/workflows/prompt-checks.yml"
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
prompt-checks:
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
- uses: actions/setup-python@v5
|
|
21
|
+
with:
|
|
22
|
+
python-version: "3.11"
|
|
23
|
+
- run: pip install instructvault
|
|
24
|
+
- run: ivault validate prompts
|
|
25
|
+
- run: ivault eval prompts/hello_world.prompt.yml --report out/report.json --junit out/junit.xml
|
|
26
|
+
- uses: actions/upload-artifact@v4
|
|
27
|
+
if: always()
|
|
28
|
+
with:
|
|
29
|
+
name: ivault-reports
|
|
30
|
+
path: out/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: instructvault
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: Git-first prompt registry + CI evals + lightweight runtime SDK (ivault).
|
|
5
5
|
Project-URL: Homepage, https://github.com/05satyam/instruct_vault
|
|
6
6
|
Project-URL: Repository, https://github.com/05satyam/instruct_vault
|
|
@@ -14,6 +14,7 @@ Requires-Dist: pyyaml>=6.0
|
|
|
14
14
|
Requires-Dist: rich>=13.7
|
|
15
15
|
Requires-Dist: typer>=0.12
|
|
16
16
|
Provides-Extra: dev
|
|
17
|
+
Requires-Dist: httpx>=0.27; extra == 'dev'
|
|
17
18
|
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
18
19
|
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
19
20
|
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
@@ -48,6 +49,10 @@ flowchart LR
|
|
|
48
49
|
Enterprises already have Git + PR reviews + CI/CD. Prompts usually don’t.
|
|
49
50
|
InstructVault brings **prompt‑as‑code** without requiring a server, database, or platform.
|
|
50
51
|
|
|
52
|
+
## Vision
|
|
53
|
+
Short version: Git‑first prompts with CI governance and zero‑latency runtime.
|
|
54
|
+
Full vision: `docs/vision.md`
|
|
55
|
+
|
|
51
56
|
## Features
|
|
52
57
|
- ✅ Git‑native versioning (tags/SHAs = releases)
|
|
53
58
|
- ✅ CLI‑first (`init`, `validate`, `render`, `eval`, `diff`, `resolve`, `bundle`)
|
|
@@ -128,6 +133,10 @@ ivault render prompts/support_reply.prompt.yml --vars '{"ticket_text":"My app cr
|
|
|
128
133
|
ivault eval prompts/support_reply.prompt.yml --dataset datasets/support_cases.jsonl --report out/report.json --junit out/junit.xml
|
|
129
134
|
```
|
|
130
135
|
|
|
136
|
+
Note: Prompts must include at least one inline test. Datasets are optional.
|
|
137
|
+
Migration tip: if you need to render a prompt that doesn’t yet include tests, use
|
|
138
|
+
`ivault render --allow-no-tests` or add a minimal test first.
|
|
139
|
+
|
|
131
140
|
### 5) Version prompts with tags
|
|
132
141
|
```bash
|
|
133
142
|
git add prompts datasets
|
|
@@ -176,7 +185,17 @@ export IVAULT_REPO_ROOT=/path/to/your/repo
|
|
|
176
185
|
PYTHONPATH=. uvicorn ivault_playground.app:app --reload
|
|
177
186
|
```
|
|
178
187
|
|
|
188
|
+

|
|
189
|
+
|
|
190
|
+
Optional auth:
|
|
191
|
+
```bash
|
|
192
|
+
export IVAULT_PLAYGROUND_API_KEY=your-secret
|
|
193
|
+
```
|
|
194
|
+
Then send `x-ivault-api-key` in requests (or keep it behind your org gateway).
|
|
195
|
+
If you don’t set the env var, no auth is required.
|
|
196
|
+
|
|
179
197
|
## Docs
|
|
198
|
+
- `docs/vision.md`
|
|
180
199
|
- `docs/governance.md`
|
|
181
200
|
- `docs/ci.md`
|
|
182
201
|
- `docs/playground.md`
|
|
@@ -25,6 +25,10 @@ flowchart LR
|
|
|
25
25
|
Enterprises already have Git + PR reviews + CI/CD. Prompts usually don’t.
|
|
26
26
|
InstructVault brings **prompt‑as‑code** without requiring a server, database, or platform.
|
|
27
27
|
|
|
28
|
+
## Vision
|
|
29
|
+
Short version: Git‑first prompts with CI governance and zero‑latency runtime.
|
|
30
|
+
Full vision: `docs/vision.md`
|
|
31
|
+
|
|
28
32
|
## Features
|
|
29
33
|
- ✅ Git‑native versioning (tags/SHAs = releases)
|
|
30
34
|
- ✅ CLI‑first (`init`, `validate`, `render`, `eval`, `diff`, `resolve`, `bundle`)
|
|
@@ -105,6 +109,10 @@ ivault render prompts/support_reply.prompt.yml --vars '{"ticket_text":"My app cr
|
|
|
105
109
|
ivault eval prompts/support_reply.prompt.yml --dataset datasets/support_cases.jsonl --report out/report.json --junit out/junit.xml
|
|
106
110
|
```
|
|
107
111
|
|
|
112
|
+
Note: Prompts must include at least one inline test. Datasets are optional.
|
|
113
|
+
Migration tip: if you need to render a prompt that doesn’t yet include tests, use
|
|
114
|
+
`ivault render --allow-no-tests` or add a minimal test first.
|
|
115
|
+
|
|
108
116
|
### 5) Version prompts with tags
|
|
109
117
|
```bash
|
|
110
118
|
git add prompts datasets
|
|
@@ -153,7 +161,17 @@ export IVAULT_REPO_ROOT=/path/to/your/repo
|
|
|
153
161
|
PYTHONPATH=. uvicorn ivault_playground.app:app --reload
|
|
154
162
|
```
|
|
155
163
|
|
|
164
|
+

|
|
165
|
+
|
|
166
|
+
Optional auth:
|
|
167
|
+
```bash
|
|
168
|
+
export IVAULT_PLAYGROUND_API_KEY=your-secret
|
|
169
|
+
```
|
|
170
|
+
Then send `x-ivault-api-key` in requests (or keep it behind your org gateway).
|
|
171
|
+
If you don’t set the env var, no auth is required.
|
|
172
|
+
|
|
156
173
|
## Docs
|
|
174
|
+
- `docs/vision.md`
|
|
157
175
|
- `docs/governance.md`
|
|
158
176
|
- `docs/ci.md`
|
|
159
177
|
- `docs/playground.md`
|
|
Binary file
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# InstructVault (ivault) — Vision
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
InstructVault makes prompts **first‑class, governed, testable, versioned artifacts** — just like code — while keeping runtime **fast and local**.
|
|
5
|
+
|
|
6
|
+
## North Star
|
|
7
|
+
**Prompts live in Git.**
|
|
8
|
+
**Prompt changes flow through CI/CD.**
|
|
9
|
+
**Prompt releases are immutable and reproducible.**
|
|
10
|
+
**Runtime stays fast, local, and framework‑agnostic.**
|
|
11
|
+
|
|
12
|
+
If InstructVault ever violates this, it is a bug — not a feature.
|
|
13
|
+
|
|
14
|
+
## Non‑Negotiables
|
|
15
|
+
1) **Git is the source of truth**
|
|
16
|
+
Prompts are files; versions are tags/SHAs/branches. No prompt database required.
|
|
17
|
+
|
|
18
|
+
2) **Zero runtime latency**
|
|
19
|
+
No network calls at inference time. Load from local repo or build‑time bundles.
|
|
20
|
+
|
|
21
|
+
3) **Framework & vendor agnostic**
|
|
22
|
+
Output is standard `{ role, content }` messages. Works with any LLM stack.
|
|
23
|
+
|
|
24
|
+
4) **Governance first**
|
|
25
|
+
Prompt changes go through PRs, reviews, and CI checks. No direct writes.
|
|
26
|
+
|
|
27
|
+
5) **Small core, extensible edges**
|
|
28
|
+
Core stays tiny and auditable. Heavy integrations are optional and separate.
|
|
29
|
+
|
|
30
|
+
## What the Core Provides
|
|
31
|
+
- Prompt spec + validation
|
|
32
|
+
- Deterministic rendering
|
|
33
|
+
- Git ref loading
|
|
34
|
+
- Deterministic evals (inline + dataset)
|
|
35
|
+
- CLI (`ivault`) and runtime SDK
|
|
36
|
+
|
|
37
|
+
## What the Core Explicitly Does Not Provide
|
|
38
|
+
- Hosted services or databases
|
|
39
|
+
- Forced LLM calls
|
|
40
|
+
- Cloud SDK dependencies in core
|
|
41
|
+
|
|
42
|
+
## Prompt Spec (YAML/JSON)
|
|
43
|
+
```yaml
|
|
44
|
+
spec_version: "1.0"
|
|
45
|
+
name: support_reply
|
|
46
|
+
variables:
|
|
47
|
+
required: [ticket_text]
|
|
48
|
+
messages:
|
|
49
|
+
- role: system
|
|
50
|
+
content: "You are a support engineer."
|
|
51
|
+
- role: user
|
|
52
|
+
content: "Ticket: {{ ticket_text }}"
|
|
53
|
+
tests:
|
|
54
|
+
- name: must_include_ticket
|
|
55
|
+
vars: { ticket_text: "Order damaged" }
|
|
56
|
+
assert: { contains_any: ["Ticket:"] }
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## CLI Contract
|
|
60
|
+
`init`, `validate`, `render`, `eval`, `diff`, `resolve`, `bundle`
|
|
61
|
+
Deterministic behavior, CI‑friendly exit codes, JSON/JUnit outputs where applicable.
|
|
62
|
+
|
|
63
|
+
## CI/CD Model (PromptOps)
|
|
64
|
+
- Run checks only when prompt paths change
|
|
65
|
+
- Stages: **validate → eval → report**
|
|
66
|
+
- Fail fast and clearly
|
|
67
|
+
|
|
68
|
+
## Governance Model
|
|
69
|
+
Use existing Git governance: CODEOWNERS, branch protection, required CI checks.
|
|
70
|
+
|
|
71
|
+
## Success Criteria
|
|
72
|
+
- Teams adopt without friction
|
|
73
|
+
- Prompt changes are reviewed like code
|
|
74
|
+
- CI catches regressions
|
|
75
|
+
- Rollbacks are instant
|
|
76
|
+
- Runtime stays fast and local
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
spec_version: "1.0"
|
|
2
|
+
name: hello_world
|
|
3
|
+
description: Minimal example prompt.
|
|
4
|
+
variables:
|
|
5
|
+
required: [name]
|
|
6
|
+
messages:
|
|
7
|
+
- role: system
|
|
8
|
+
content: "You are a helpful assistant."
|
|
9
|
+
- role: user
|
|
10
|
+
content: "Say hello to {{ name }}."
|
|
11
|
+
tests:
|
|
12
|
+
- name: includes_name
|
|
13
|
+
vars: { name: "Ava" }
|
|
14
|
+
assert: { contains_any: ["Ava"] }
|
|
@@ -17,5 +17,20 @@ export IVAULT_REPO_ROOT=/path/to/your/repo
|
|
|
17
17
|
PYTHONPATH=.. uvicorn ivault_playground.app:app --reload
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
+
Alternative (from repo root):
|
|
21
|
+
```
|
|
22
|
+
export IVAULT_REPO_ROOT=/path/to/your/repo
|
|
23
|
+
PYTHONPATH=. uvicorn ivault_playground.app:app --reload
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Then open:
|
|
27
|
+
- `http://127.0.0.1:8000/` (landing page)
|
|
28
|
+
- `http://127.0.0.1:8000/docs` (API docs)
|
|
29
|
+
|
|
20
30
|
Environment:
|
|
21
31
|
- `IVAULT_REPO_ROOT` (default: current working directory)
|
|
32
|
+
- `IVAULT_PLAYGROUND_API_KEY` (optional; if set, require `x-ivault-api-key` header)
|
|
33
|
+
|
|
34
|
+
Notes:
|
|
35
|
+
- This minimal playground has no auth; put it behind your org auth if hosted.
|
|
36
|
+
- PR-only writes are not implemented yet (API is read-only + eval).
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import os
|
|
3
|
+
from fastapi import FastAPI, Request
|
|
4
|
+
from fastapi.responses import HTMLResponse
|
|
5
|
+
from fastapi.staticfiles import StaticFiles
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from .routes.api import router as api_router
|
|
8
|
+
from .routes.ui import router as ui_router
|
|
9
|
+
|
|
10
|
+
app = FastAPI(title="ivault-playground", version="0.1.0")
|
|
11
|
+
|
|
12
|
+
_API_KEY = os.getenv("IVAULT_PLAYGROUND_API_KEY")
|
|
13
|
+
|
|
14
|
+
@app.middleware("http")
|
|
15
|
+
async def _api_key_guard(request: Request, call_next):
|
|
16
|
+
if not _API_KEY:
|
|
17
|
+
return await call_next(request)
|
|
18
|
+
if request.url.path in ("/health",):
|
|
19
|
+
return await call_next(request)
|
|
20
|
+
key = request.headers.get("x-ivault-api-key")
|
|
21
|
+
if key != _API_KEY:
|
|
22
|
+
return HTMLResponse("Unauthorized", status_code=401)
|
|
23
|
+
return await call_next(request)
|
|
24
|
+
|
|
25
|
+
app.include_router(ui_router)
|
|
26
|
+
app.include_router(api_router)
|
|
27
|
+
|
|
28
|
+
_STATIC_DIR = Path(__file__).resolve().parent / "static"
|
|
29
|
+
app.mount("/static", StaticFiles(directory=_STATIC_DIR), name="static")
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import os
|
|
3
|
+
import subprocess
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter, HTTPException
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
from instructvault import InstructVault
|
|
11
|
+
from instructvault.io import load_prompt_spec, load_dataset_jsonl
|
|
12
|
+
from instructvault.store import PromptStore
|
|
13
|
+
from instructvault.eval import run_dataset, run_inline_tests
|
|
14
|
+
|
|
15
|
+
router = APIRouter()
|
|
16
|
+
|
|
17
|
+
class RenderRequest(BaseModel):
|
|
18
|
+
prompt_path: str
|
|
19
|
+
vars: Dict[str, Any]
|
|
20
|
+
ref: Optional[str] = None
|
|
21
|
+
|
|
22
|
+
class EvalRequest(BaseModel):
|
|
23
|
+
prompt_path: str
|
|
24
|
+
dataset_path: Optional[str] = None
|
|
25
|
+
ref: Optional[str] = None
|
|
26
|
+
|
|
27
|
+
def _repo_root() -> Path:
|
|
28
|
+
return Path(os.getenv("IVAULT_REPO_ROOT", ".")).resolve()
|
|
29
|
+
|
|
30
|
+
@router.get("/health")
|
|
31
|
+
def health() -> Dict[str, str]:
|
|
32
|
+
return {"status": "ok"}
|
|
33
|
+
|
|
34
|
+
@router.get("/prompts")
|
|
35
|
+
def list_prompts(ref: Optional[str] = None) -> List[str]:
|
|
36
|
+
repo = _repo_root()
|
|
37
|
+
prompts_dir = repo / "prompts"
|
|
38
|
+
if ref:
|
|
39
|
+
cmd = ["git", "-C", str(repo), "ls-tree", "-r", "--name-only", ref, "prompts"]
|
|
40
|
+
res = subprocess.run(cmd, capture_output=True, text=True)
|
|
41
|
+
if res.returncode != 0:
|
|
42
|
+
return []
|
|
43
|
+
files = [line.strip() for line in res.stdout.splitlines() if line.strip()]
|
|
44
|
+
return [p for p in files if p.endswith((".prompt.yml", ".prompt.yaml", ".prompt.json"))]
|
|
45
|
+
if not prompts_dir.exists():
|
|
46
|
+
return []
|
|
47
|
+
files = sorted(prompts_dir.rglob("*.prompt.y*ml")) + sorted(prompts_dir.rglob("*.prompt.json"))
|
|
48
|
+
return [p.relative_to(repo).as_posix() for p in files]
|
|
49
|
+
|
|
50
|
+
@router.get("/refs")
|
|
51
|
+
def list_refs() -> List[str]:
|
|
52
|
+
repo = _repo_root()
|
|
53
|
+
cmd = ["git", "-C", str(repo), "tag", "--list", "prompts/*"]
|
|
54
|
+
res = subprocess.run(cmd, capture_output=True, text=True)
|
|
55
|
+
if res.returncode != 0:
|
|
56
|
+
return []
|
|
57
|
+
return [r.strip() for r in res.stdout.splitlines() if r.strip()]
|
|
58
|
+
|
|
59
|
+
@router.get("/prompt")
|
|
60
|
+
def get_prompt(prompt_path: str, ref: Optional[str] = None) -> Dict[str, Any]:
|
|
61
|
+
repo = _repo_root()
|
|
62
|
+
if ref:
|
|
63
|
+
store = PromptStore(repo)
|
|
64
|
+
try:
|
|
65
|
+
spec = load_prompt_spec(store.read_text(prompt_path, ref=ref))
|
|
66
|
+
except Exception:
|
|
67
|
+
raise HTTPException(status_code=404, detail="Prompt not found at ref")
|
|
68
|
+
else:
|
|
69
|
+
p = repo / prompt_path
|
|
70
|
+
if not p.exists():
|
|
71
|
+
raise HTTPException(status_code=404, detail="Prompt not found")
|
|
72
|
+
spec = load_prompt_spec(p.read_text(encoding="utf-8"))
|
|
73
|
+
return spec.model_dump(by_alias=True)
|
|
74
|
+
|
|
75
|
+
@router.post("/render")
|
|
76
|
+
def render(req: RenderRequest) -> List[Dict[str, str]]:
|
|
77
|
+
repo = _repo_root()
|
|
78
|
+
vault = InstructVault(repo_root=repo)
|
|
79
|
+
msgs = vault.render(req.prompt_path, vars=req.vars, ref=req.ref)
|
|
80
|
+
return [{"role": m.role, "content": m.content} for m in msgs]
|
|
81
|
+
|
|
82
|
+
@router.post("/eval")
|
|
83
|
+
def eval_prompt(req: EvalRequest) -> Dict[str, Any]:
|
|
84
|
+
repo = _repo_root()
|
|
85
|
+
store = PromptStore(repo)
|
|
86
|
+
spec = load_prompt_spec(store.read_text(req.prompt_path, ref=req.ref))
|
|
87
|
+
|
|
88
|
+
ok1, r1 = run_inline_tests(spec)
|
|
89
|
+
results = list(r1)
|
|
90
|
+
ok = ok1
|
|
91
|
+
|
|
92
|
+
if req.dataset_path:
|
|
93
|
+
rows = load_dataset_jsonl((repo / req.dataset_path).read_text(encoding="utf-8"))
|
|
94
|
+
ok2, r2 = run_dataset(spec, rows)
|
|
95
|
+
ok = ok and ok2
|
|
96
|
+
results.extend(r2)
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
"prompt": spec.name,
|
|
100
|
+
"ref": req.ref or "WORKTREE",
|
|
101
|
+
"pass": ok,
|
|
102
|
+
"results": [{"test": r.name, "pass": r.passed, "error": r.error} for r in results],
|
|
103
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from fastapi import APIRouter
|
|
3
|
+
from fastapi.responses import HTMLResponse
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
router = APIRouter()
|
|
7
|
+
|
|
8
|
+
_TEMPLATE = Path(__file__).resolve().parent.parent / "templates" / "index.html"
|
|
9
|
+
|
|
10
|
+
@router.get("/", response_class=HTMLResponse)
|
|
11
|
+
def index() -> str:
|
|
12
|
+
return _TEMPLATE.read_text(encoding="utf-8")
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--bg: #0f172a;
|
|
3
|
+
--panel: #111827;
|
|
4
|
+
--muted: #94a3b8;
|
|
5
|
+
--text: #e2e8f0;
|
|
6
|
+
--accent: #22c55e;
|
|
7
|
+
--line: #1f2937;
|
|
8
|
+
}
|
|
9
|
+
body {
|
|
10
|
+
margin: 0;
|
|
11
|
+
font-family: "IBM Plex Sans", "Segoe UI", "Helvetica Neue", Arial, sans-serif;
|
|
12
|
+
background: radial-gradient(1200px 600px at 10% -10%, #1e293b, #0f172a);
|
|
13
|
+
color: var(--text);
|
|
14
|
+
}
|
|
15
|
+
.wrap { max-width: 980px; margin: 48px auto; padding: 0 24px; }
|
|
16
|
+
.hero {
|
|
17
|
+
background: linear-gradient(140deg, #0b1220, #111827);
|
|
18
|
+
border: 1px solid var(--line);
|
|
19
|
+
border-radius: 16px;
|
|
20
|
+
padding: 32px;
|
|
21
|
+
box-shadow: 0 10px 30px rgba(0,0,0,0.35);
|
|
22
|
+
}
|
|
23
|
+
h1 { margin: 0 0 8px 0; font-size: 32px; letter-spacing: 0.3px; }
|
|
24
|
+
.sub { color: var(--muted); margin: 0 0 24px 0; }
|
|
25
|
+
.grid {
|
|
26
|
+
display: grid;
|
|
27
|
+
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
|
28
|
+
gap: 16px;
|
|
29
|
+
}
|
|
30
|
+
.split {
|
|
31
|
+
display: grid;
|
|
32
|
+
grid-template-columns: 1fr 1fr;
|
|
33
|
+
gap: 16px;
|
|
34
|
+
margin-top: 20px;
|
|
35
|
+
}
|
|
36
|
+
.panel {
|
|
37
|
+
background: var(--panel);
|
|
38
|
+
border: 1px solid var(--line);
|
|
39
|
+
border-radius: 12px;
|
|
40
|
+
padding: 16px;
|
|
41
|
+
}
|
|
42
|
+
.panel h3 { margin: 0 0 8px 0; font-size: 16px; }
|
|
43
|
+
.row {
|
|
44
|
+
display: flex;
|
|
45
|
+
gap: 8px;
|
|
46
|
+
align-items: center;
|
|
47
|
+
margin-bottom: 12px;
|
|
48
|
+
}
|
|
49
|
+
.col {
|
|
50
|
+
display: flex;
|
|
51
|
+
flex-direction: column;
|
|
52
|
+
gap: 8px;
|
|
53
|
+
}
|
|
54
|
+
select {
|
|
55
|
+
background: #0b1220;
|
|
56
|
+
color: var(--text);
|
|
57
|
+
border: 1px solid var(--line);
|
|
58
|
+
border-radius: 8px;
|
|
59
|
+
padding: 6px 8px;
|
|
60
|
+
}
|
|
61
|
+
textarea {
|
|
62
|
+
width: 100%;
|
|
63
|
+
min-height: 90px;
|
|
64
|
+
background: #0b1220;
|
|
65
|
+
color: var(--text);
|
|
66
|
+
border: 1px solid var(--line);
|
|
67
|
+
border-radius: 8px;
|
|
68
|
+
padding: 10px;
|
|
69
|
+
font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
|
70
|
+
font-size: 12px;
|
|
71
|
+
}
|
|
72
|
+
.btn {
|
|
73
|
+
background: #16a34a;
|
|
74
|
+
color: #0b1220;
|
|
75
|
+
border: none;
|
|
76
|
+
border-radius: 8px;
|
|
77
|
+
padding: 8px 12px;
|
|
78
|
+
cursor: pointer;
|
|
79
|
+
font-weight: 600;
|
|
80
|
+
}
|
|
81
|
+
.btn.secondary {
|
|
82
|
+
background: transparent;
|
|
83
|
+
color: var(--text);
|
|
84
|
+
border: 1px solid var(--line);
|
|
85
|
+
}
|
|
86
|
+
.list {
|
|
87
|
+
max-height: 320px;
|
|
88
|
+
overflow: auto;
|
|
89
|
+
border: 1px solid var(--line);
|
|
90
|
+
border-radius: 8px;
|
|
91
|
+
}
|
|
92
|
+
.list button {
|
|
93
|
+
display: block;
|
|
94
|
+
width: 100%;
|
|
95
|
+
text-align: left;
|
|
96
|
+
padding: 10px 12px;
|
|
97
|
+
border: none;
|
|
98
|
+
background: transparent;
|
|
99
|
+
color: var(--text);
|
|
100
|
+
border-bottom: 1px solid var(--line);
|
|
101
|
+
cursor: pointer;
|
|
102
|
+
}
|
|
103
|
+
.list button:hover { background: rgba(148,163,184,0.08); }
|
|
104
|
+
pre {
|
|
105
|
+
background: #0b1220;
|
|
106
|
+
border: 1px solid var(--line);
|
|
107
|
+
border-radius: 8px;
|
|
108
|
+
padding: 12px;
|
|
109
|
+
color: #e5e7eb;
|
|
110
|
+
overflow: auto;
|
|
111
|
+
max-height: 320px;
|
|
112
|
+
}
|
|
113
|
+
.card {
|
|
114
|
+
background: var(--panel);
|
|
115
|
+
border: 1px solid var(--line);
|
|
116
|
+
border-radius: 12px;
|
|
117
|
+
padding: 16px;
|
|
118
|
+
}
|
|
119
|
+
.card h3 { margin: 0 0 8px 0; font-size: 16px; }
|
|
120
|
+
.card p { margin: 0; color: var(--muted); font-size: 14px; }
|
|
121
|
+
.links a {
|
|
122
|
+
display: inline-block;
|
|
123
|
+
margin-right: 12px;
|
|
124
|
+
color: var(--text);
|
|
125
|
+
text-decoration: none;
|
|
126
|
+
border-bottom: 1px solid transparent;
|
|
127
|
+
}
|
|
128
|
+
.links a:hover { border-bottom-color: var(--accent); }
|
|
129
|
+
.badge {
|
|
130
|
+
display: inline-block;
|
|
131
|
+
padding: 4px 10px;
|
|
132
|
+
border-radius: 999px;
|
|
133
|
+
background: rgba(34,197,94,0.15);
|
|
134
|
+
color: #86efac;
|
|
135
|
+
font-size: 12px;
|
|
136
|
+
margin-left: 8px;
|
|
137
|
+
}
|
|
138
|
+
footer { margin-top: 24px; color: var(--muted); font-size: 12px; }
|
|
139
|
+
@media (max-width: 900px) {
|
|
140
|
+
.split { grid-template-columns: 1fr; }
|
|
141
|
+
}
|
|
142
|
+
@media (max-width: 640px) {
|
|
143
|
+
.wrap { margin: 24px auto; }
|
|
144
|
+
h1 { font-size: 26px; }
|
|
145
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
async function loadPrompts() {
|
|
2
|
+
const list = document.getElementById("promptList");
|
|
3
|
+
list.innerHTML = "Loading...";
|
|
4
|
+
try {
|
|
5
|
+
const ref = document.getElementById("refSelect").value;
|
|
6
|
+
const url = ref ? `/prompts?ref=${encodeURIComponent(ref)}` : "/prompts";
|
|
7
|
+
const res = await fetch(url);
|
|
8
|
+
const data = await res.json();
|
|
9
|
+
list.innerHTML = "";
|
|
10
|
+
if (!data.length) {
|
|
11
|
+
list.innerHTML = "<div style='padding:10px;color:#94a3b8;'>No prompts found.</div>";
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
data.forEach((p) => {
|
|
15
|
+
const btn = document.createElement("button");
|
|
16
|
+
btn.textContent = p;
|
|
17
|
+
btn.onclick = () => loadPrompt(p);
|
|
18
|
+
list.appendChild(btn);
|
|
19
|
+
});
|
|
20
|
+
} catch (e) {
|
|
21
|
+
list.innerHTML = "<div style='padding:10px;color:#f87171;'>Failed to load prompts.</div>";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function loadPrompt(path) {
|
|
26
|
+
const preview = document.getElementById("promptPreview");
|
|
27
|
+
preview.textContent = "Loading...";
|
|
28
|
+
try {
|
|
29
|
+
const ref = document.getElementById("refSelect").value;
|
|
30
|
+
const url = ref
|
|
31
|
+
? `/prompt?prompt_path=${encodeURIComponent(path)}&ref=${encodeURIComponent(ref)}`
|
|
32
|
+
: `/prompt?prompt_path=${encodeURIComponent(path)}`;
|
|
33
|
+
const res = await fetch(url);
|
|
34
|
+
const data = await res.json();
|
|
35
|
+
preview.textContent = JSON.stringify(data, null, 2);
|
|
36
|
+
window.__currentPromptPath = path;
|
|
37
|
+
window.__currentPromptRef = ref || null;
|
|
38
|
+
} catch (e) {
|
|
39
|
+
preview.textContent = "Failed to load prompt.";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function loadRefs() {
|
|
44
|
+
try {
|
|
45
|
+
const res = await fetch("/refs");
|
|
46
|
+
const data = await res.json();
|
|
47
|
+
const sel = document.getElementById("refSelect");
|
|
48
|
+
data.forEach((r) => {
|
|
49
|
+
const opt = document.createElement("option");
|
|
50
|
+
opt.value = r;
|
|
51
|
+
opt.textContent = r;
|
|
52
|
+
sel.appendChild(opt);
|
|
53
|
+
});
|
|
54
|
+
sel.onchange = () => {
|
|
55
|
+
document.getElementById("promptPreview").textContent = "Select a prompt to view its spec.";
|
|
56
|
+
document.getElementById("renderOutput").textContent = "Rendered messages will appear here.";
|
|
57
|
+
window.__currentPromptPath = null;
|
|
58
|
+
window.__currentPromptRef = sel.value || null;
|
|
59
|
+
loadPrompts();
|
|
60
|
+
};
|
|
61
|
+
} catch (e) {}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function attachHandlers() {
|
|
65
|
+
document.getElementById("varsInput").value = JSON.stringify({name: "ivault"});
|
|
66
|
+
|
|
67
|
+
document.getElementById("copyBtn").onclick = async () => {
|
|
68
|
+
const text = document.getElementById("promptPreview").textContent;
|
|
69
|
+
try {
|
|
70
|
+
await navigator.clipboard.writeText(text);
|
|
71
|
+
document.getElementById("copyBtn").textContent = "Copied";
|
|
72
|
+
setTimeout(() => (document.getElementById("copyBtn").textContent = "Copy JSON"), 1200);
|
|
73
|
+
} catch (e) {}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
document.getElementById("renderBtn").onclick = async () => {
|
|
77
|
+
const out = document.getElementById("renderOutput");
|
|
78
|
+
out.textContent = "Rendering...";
|
|
79
|
+
try {
|
|
80
|
+
const path = window.__currentPromptPath;
|
|
81
|
+
if (!path) {
|
|
82
|
+
out.textContent = "Select a prompt first.";
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const varsText = document.getElementById("varsInput").value || "{}";
|
|
86
|
+
let vars;
|
|
87
|
+
try {
|
|
88
|
+
vars = JSON.parse(varsText);
|
|
89
|
+
} catch (e) {
|
|
90
|
+
out.textContent = 'Invalid JSON in vars. Example: {"name":"Ava"}';
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const ref = window.__currentPromptRef;
|
|
94
|
+
const payload = { prompt_path: path, vars, ref };
|
|
95
|
+
const res = await fetch("/render", {
|
|
96
|
+
method: "POST",
|
|
97
|
+
headers: { "Content-Type": "application/json" },
|
|
98
|
+
body: JSON.stringify(payload),
|
|
99
|
+
});
|
|
100
|
+
const data = await res.json();
|
|
101
|
+
if (!res.ok) {
|
|
102
|
+
out.textContent = data.detail || "Render failed.";
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
out.textContent = JSON.stringify(data, null, 2);
|
|
106
|
+
} catch (e) {
|
|
107
|
+
out.textContent = "Render failed. Check JSON vars.";
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
window.addEventListener("load", () => {
|
|
113
|
+
loadPrompts();
|
|
114
|
+
loadRefs();
|
|
115
|
+
attachHandlers();
|
|
116
|
+
});
|