pi-para 0.6.0 → 0.6.1
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.
- package/ARCHITECTURE.md +0 -9
- package/CHANGELOG.md +13 -0
- package/CONFIGURATION.md +0 -23
- package/README.md +1 -29
- package/SETUP.md +1 -1
- package/STRUCTURE.md +0 -9
- package/dist/atomic-write.d.ts +2 -0
- package/dist/atomic-write.d.ts.map +1 -0
- package/dist/atomic-write.js +18 -0
- package/dist/atomic-write.js.map +1 -0
- package/dist/capture.d.ts +34 -0
- package/dist/capture.d.ts.map +1 -0
- package/dist/capture.js +461 -0
- package/dist/capture.js.map +1 -0
- package/dist/cli-legacy.d.ts +4 -0
- package/dist/cli-legacy.d.ts.map +1 -0
- package/dist/cli-legacy.js +41 -0
- package/dist/cli-legacy.js.map +1 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +427 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands.d.ts +15 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +719 -0
- package/dist/commands.js.map +1 -0
- package/dist/config.d.ts +94 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +228 -0
- package/dist/config.js.map +1 -0
- package/dist/context.d.ts +63 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +311 -0
- package/dist/context.js.map +1 -0
- package/dist/credentials.d.ts +27 -0
- package/dist/credentials.d.ts.map +1 -0
- package/dist/credentials.js +85 -0
- package/dist/credentials.js.map +1 -0
- package/dist/daemon.d.ts +49 -0
- package/dist/daemon.d.ts.map +1 -0
- package/dist/daemon.js +226 -0
- package/dist/daemon.js.map +1 -0
- package/dist/doctor.d.ts +20 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +176 -0
- package/dist/doctor.js.map +1 -0
- package/dist/frontmatter.d.ts +59 -0
- package/dist/frontmatter.d.ts.map +1 -0
- package/dist/frontmatter.js +170 -0
- package/dist/frontmatter.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +342 -0
- package/dist/index.js.map +1 -0
- package/dist/ingest.d.ts +88 -0
- package/dist/ingest.d.ts.map +1 -0
- package/dist/ingest.js +105 -0
- package/dist/ingest.js.map +1 -0
- package/dist/link-discovery.d.ts +40 -0
- package/dist/link-discovery.d.ts.map +1 -0
- package/dist/link-discovery.js +122 -0
- package/dist/link-discovery.js.map +1 -0
- package/dist/link-utils.d.ts +30 -0
- package/dist/link-utils.d.ts.map +1 -0
- package/dist/link-utils.js +145 -0
- package/dist/link-utils.js.map +1 -0
- package/dist/lint.d.ts +39 -0
- package/dist/lint.d.ts.map +1 -0
- package/dist/lint.js +827 -0
- package/dist/lint.js.map +1 -0
- package/dist/maintainer.d.ts +24 -0
- package/dist/maintainer.d.ts.map +1 -0
- package/dist/maintainer.js +358 -0
- package/dist/maintainer.js.map +1 -0
- package/dist/model-resolver.d.ts +30 -0
- package/dist/model-resolver.d.ts.map +1 -0
- package/dist/model-resolver.js +71 -0
- package/dist/model-resolver.js.map +1 -0
- package/dist/paths.d.ts +20 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +33 -0
- package/dist/paths.js.map +1 -0
- package/dist/processor.d.ts +14 -0
- package/dist/processor.d.ts.map +1 -0
- package/dist/processor.js +361 -0
- package/dist/processor.js.map +1 -0
- package/dist/qmd-providers.d.ts +12 -0
- package/dist/qmd-providers.d.ts.map +1 -0
- package/dist/qmd-providers.js +45 -0
- package/dist/qmd-providers.js.map +1 -0
- package/dist/query.d.ts +69 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +120 -0
- package/dist/query.js.map +1 -0
- package/dist/raw.d.ts +57 -0
- package/dist/raw.d.ts.map +1 -0
- package/dist/raw.js +219 -0
- package/dist/raw.js.map +1 -0
- package/dist/redact.d.ts +21 -0
- package/dist/redact.d.ts.map +1 -0
- package/dist/redact.js +143 -0
- package/dist/redact.js.map +1 -0
- package/dist/repair.d.ts +11 -0
- package/dist/repair.d.ts.map +1 -0
- package/dist/repair.js +41 -0
- package/dist/repair.js.map +1 -0
- package/dist/scheduler/controls.d.ts +35 -0
- package/dist/scheduler/controls.d.ts.map +1 -0
- package/dist/scheduler/controls.js +98 -0
- package/dist/scheduler/controls.js.map +1 -0
- package/dist/scheduler/index.d.ts +48 -0
- package/dist/scheduler/index.d.ts.map +1 -0
- package/dist/scheduler/index.js +193 -0
- package/dist/scheduler/index.js.map +1 -0
- package/dist/scheduler/leases.d.ts +22 -0
- package/dist/scheduler/leases.d.ts.map +1 -0
- package/dist/scheduler/leases.js +74 -0
- package/dist/scheduler/leases.js.map +1 -0
- package/dist/scheduler/session-capture.d.ts +33 -0
- package/dist/scheduler/session-capture.d.ts.map +1 -0
- package/dist/scheduler/session-capture.js +108 -0
- package/dist/scheduler/session-capture.js.map +1 -0
- package/dist/scheduler/state.d.ts +56 -0
- package/dist/scheduler/state.d.ts.map +1 -0
- package/dist/scheduler/state.js +210 -0
- package/dist/scheduler/state.js.map +1 -0
- package/dist/scope.d.ts +54 -0
- package/dist/scope.d.ts.map +1 -0
- package/dist/scope.js +161 -0
- package/dist/scope.js.map +1 -0
- package/dist/scripts/bulk-link-fix.d.ts +9 -0
- package/dist/scripts/bulk-link-fix.d.ts.map +1 -0
- package/dist/scripts/bulk-link-fix.js +106 -0
- package/dist/scripts/bulk-link-fix.js.map +1 -0
- package/dist/session-tools.d.ts +33 -0
- package/dist/session-tools.d.ts.map +1 -0
- package/dist/session-tools.js +211 -0
- package/dist/session-tools.js.map +1 -0
- package/dist/settings.d.ts +19 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/settings.js +106 -0
- package/dist/settings.js.map +1 -0
- package/dist/setup.d.ts +29 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +90 -0
- package/dist/setup.js.map +1 -0
- package/dist/state.d.ts +54 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +176 -0
- package/dist/state.js.map +1 -0
- package/dist/status.d.ts +22 -0
- package/dist/status.d.ts.map +1 -0
- package/dist/status.js +93 -0
- package/dist/status.js.map +1 -0
- package/dist/store.d.ts +71 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +282 -0
- package/dist/store.js.map +1 -0
- package/dist/summarize.d.ts +60 -0
- package/dist/summarize.d.ts.map +1 -0
- package/dist/summarize.js +266 -0
- package/dist/summarize.js.map +1 -0
- package/dist/tag-registry.d.ts +56 -0
- package/dist/tag-registry.d.ts.map +1 -0
- package/dist/tag-registry.js +146 -0
- package/dist/tag-registry.js.map +1 -0
- package/dist/templates/prompts.d.ts +51 -0
- package/dist/templates/prompts.d.ts.map +1 -0
- package/dist/templates/prompts.js +203 -0
- package/dist/templates/prompts.js.map +1 -0
- package/dist/tools.d.ts +26 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +1198 -0
- package/dist/tools.js.map +1 -0
- package/dist/watcher.d.ts +25 -0
- package/dist/watcher.d.ts.map +1 -0
- package/dist/watcher.js +84 -0
- package/dist/watcher.js.map +1 -0
- package/dist/webui/client/dist/index.html +1885 -0
- package/dist/webui/server.d.ts +31 -0
- package/dist/webui/server.d.ts.map +1 -0
- package/dist/webui/server.js +566 -0
- package/dist/webui/server.js.map +1 -0
- package/dist/wiki-tool-guidance.d.ts +6 -0
- package/dist/wiki-tool-guidance.d.ts.map +1 -0
- package/dist/wiki-tool-guidance.js +70 -0
- package/dist/wiki-tool-guidance.js.map +1 -0
- package/dist/wiki.d.ts +108 -0
- package/dist/wiki.d.ts.map +1 -0
- package/dist/wiki.js +497 -0
- package/dist/wiki.js.map +1 -0
- package/package.json +2 -1
- package/scripts/build-assets.mjs +1 -13
- package/skills/setup/SKILL.md +1 -1
- package/src/cli.ts +0 -70
- package/src/config.ts +0 -25
- package/src/index.ts +1 -38
- package/src/templates/prompts.ts +0 -56
- package/scripts/gepa/dataset.py +0 -192
- package/scripts/gepa/lm_providers.py +0 -472
- package/scripts/gepa/metric.py +0 -279
- package/scripts/gepa/optimize.py +0 -367
- package/scripts/gepa/program.py +0 -99
- package/scripts/gepa/pyproject.toml +0 -19
- package/scripts/gepa/run-all.sh +0 -53
- package/scripts/gepa/wiki_reader.py +0 -113
- package/src/gepa/index.ts +0 -287
- package/src/gepa/optimized/capture-prompt.txt +0 -65
package/scripts/gepa/metric.py
DELETED
|
@@ -1,279 +0,0 @@
|
|
|
1
|
-
"""LLM-as-judge metric for GEPA prompt optimization.
|
|
2
|
-
|
|
3
|
-
Returns dspy.Prediction(score=float, feedback=str) — GEPA reads the
|
|
4
|
-
textual feedback to guide instruction evolution.
|
|
5
|
-
|
|
6
|
-
One LLM judge call per (candidate, example) evaluation.
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
from __future__ import annotations
|
|
10
|
-
|
|
11
|
-
import json
|
|
12
|
-
import logging
|
|
13
|
-
import re
|
|
14
|
-
|
|
15
|
-
import dspy
|
|
16
|
-
|
|
17
|
-
logger = logging.getLogger(__name__)
|
|
18
|
-
|
|
19
|
-
# -- Judge prompt template ----------------------------------------------------
|
|
20
|
-
|
|
21
|
-
JUDGE_SYSTEM = """You are an expert evaluator for PARA wiki knowledge management prompts and instructions.
|
|
22
|
-
|
|
23
|
-
You score how well a wiki system's output follows the PARA methodology:
|
|
24
|
-
- **Projects/**: Goals with end dates
|
|
25
|
-
- **Areas/**: Ongoing responsibilities
|
|
26
|
-
- **Resources/**: Reference docs, how-tos, patterns (default for most pages)
|
|
27
|
-
- **Archives/**: Completed items
|
|
28
|
-
|
|
29
|
-
Good wiki output has: structured sections (Topic, Key Facts, Connections),
|
|
30
|
-
[[wikilinks]] to related pages, kebab-case scope/tags, no exposed secrets,
|
|
31
|
-
and clear actionable content."""
|
|
32
|
-
|
|
33
|
-
JUDGE_PROMPT = """Evaluate the following wiki output produced by an instruction/prompt.
|
|
34
|
-
|
|
35
|
-
TARGET TYPE: {target_type}
|
|
36
|
-
TARGET PURPOSE: {target_purpose}
|
|
37
|
-
|
|
38
|
-
TASK CONTEXT (input given to the instruction):
|
|
39
|
-
{task_context}
|
|
40
|
-
|
|
41
|
-
PRODUCED OUTPUT (generated using the instruction):
|
|
42
|
-
{produced_output}
|
|
43
|
-
|
|
44
|
-
REFERENCE OUTPUT (real wiki page for comparison):
|
|
45
|
-
{expected_output}
|
|
46
|
-
|
|
47
|
-
Score on these 6 dimensions (each 0.0 to 1.0):
|
|
48
|
-
|
|
49
|
-
1. **Structure**: Has proper sections (Topic, Key Facts, Connections). Uses markdown headers. Reasonable length (200-5000 chars for body). Not just a wall of text.
|
|
50
|
-
2. **PARA Compliance**: Would produce correct category assignment. Scope mentions are kebab-case project names (not topic descriptions). Tags would be kebab-case.
|
|
51
|
-
3. **Cross-references**: Mentions or produces [[wikilinks]]. References related concepts. Would include a Connections section.
|
|
52
|
-
4. **Security**: Does not expose API keys, tokens, passwords. Documents WHERE secrets are stored, not values.
|
|
53
|
-
5. **Completeness**: Covers the topic adequately. Includes key facts. Doesn't miss obvious important aspects.
|
|
54
|
-
6. **Actionability**: Output is clear and specific. Follows concrete rules. Not vague or ambiguous.
|
|
55
|
-
|
|
56
|
-
Respond ONLY with valid JSON (no markdown fences):
|
|
57
|
-
{{"dimensions": [{{"name": "structure", "score": 0.0, "feedback": "..."}}, {{"name": "para_compliance", "score": 0.0, "feedback": "..."}}, {{"name": "cross_references", "score": 0.0, "feedback": "..."}}, {{"name": "security", "score": 0.0, "feedback": "..."}}, {{"name": "completeness", "score": 0.0, "feedback": "..."}}, {{"name": "actionability", "score": 0.0, "feedback": "..."}}], "overall_feedback": "..."}}"""
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
# -- Dimension weights --------------------------------------------------------
|
|
61
|
-
|
|
62
|
-
DIMENSION_WEIGHTS = {
|
|
63
|
-
"structure": 0.15,
|
|
64
|
-
"para_compliance": 0.20,
|
|
65
|
-
"cross_references": 0.15,
|
|
66
|
-
"security": 0.15,
|
|
67
|
-
"completeness": 0.15,
|
|
68
|
-
"actionability": 0.20,
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
# -- Target purposes ----------------------------------------------------------
|
|
72
|
-
|
|
73
|
-
TARGET_PURPOSES = {
|
|
74
|
-
"capture": "Guide an LLM to extract knowledge from coding sessions into PARA wiki pages",
|
|
75
|
-
"ingest": "Guide an LLM to organize ingested source material into structured wiki pages",
|
|
76
|
-
"query": "Guide an LLM to synthesize answers from wiki search results with citations",
|
|
77
|
-
"maintenance": "Guide an LLM to maintain wiki quality: dedup, links, categories, staleness",
|
|
78
|
-
"tool": "Describe a wiki tool's purpose so the LLM knows when and how to use it",
|
|
79
|
-
"skill": "Behavioral guidelines for actively using the PARA wiki during work",
|
|
80
|
-
"system": "System prompt for a standalone wiki knowledge management agent",
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
# -- JSON parsing helpers -----------------------------------------------------
|
|
85
|
-
|
|
86
|
-
def _parse_judge_response(text: str) -> dict | None:
|
|
87
|
-
"""Parse JSON from LLM judge response, handling markdown fences."""
|
|
88
|
-
# Strip markdown code fences
|
|
89
|
-
text = text.strip()
|
|
90
|
-
if text.startswith("```"):
|
|
91
|
-
# Remove first and last lines (fences)
|
|
92
|
-
lines = text.split("\n")
|
|
93
|
-
text = "\n".join(lines[1:-1] if lines[-1].strip() == "```" else lines[1:])
|
|
94
|
-
|
|
95
|
-
# Try direct parse
|
|
96
|
-
try:
|
|
97
|
-
return json.loads(text)
|
|
98
|
-
except json.JSONDecodeError:
|
|
99
|
-
pass
|
|
100
|
-
|
|
101
|
-
# Try finding JSON in the text
|
|
102
|
-
match = re.search(r'\{.*\}', text, re.DOTALL)
|
|
103
|
-
if match:
|
|
104
|
-
try:
|
|
105
|
-
return json.loads(match.group())
|
|
106
|
-
except json.JSONDecodeError:
|
|
107
|
-
pass
|
|
108
|
-
|
|
109
|
-
return None
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
def _compute_score(parsed: dict) -> tuple[float, str]:
|
|
113
|
-
"""Compute weighted score and formatted feedback from parsed judge response."""
|
|
114
|
-
dimensions = parsed.get("dimensions", [])
|
|
115
|
-
if not dimensions:
|
|
116
|
-
return 0.0, "No dimensions in judge response"
|
|
117
|
-
|
|
118
|
-
total_score = 0.0
|
|
119
|
-
total_weight = 0.0
|
|
120
|
-
feedback_parts = []
|
|
121
|
-
|
|
122
|
-
for dim in dimensions:
|
|
123
|
-
name = dim.get("name", "unknown")
|
|
124
|
-
score = float(dim.get("score", 0.0))
|
|
125
|
-
fb = dim.get("feedback", "")
|
|
126
|
-
weight = DIMENSION_WEIGHTS.get(name, 1.0 / len(dimensions))
|
|
127
|
-
|
|
128
|
-
total_score += score * weight
|
|
129
|
-
total_weight += weight
|
|
130
|
-
feedback_parts.append(f"{name}={score:.1f}: {fb}")
|
|
131
|
-
|
|
132
|
-
overall = parsed.get("overall_feedback", "")
|
|
133
|
-
if overall:
|
|
134
|
-
feedback_parts.append(f"Overall: {overall}")
|
|
135
|
-
|
|
136
|
-
final_score = total_score / total_weight if total_weight > 0 else 0.0
|
|
137
|
-
feedback = " | ".join(feedback_parts)
|
|
138
|
-
|
|
139
|
-
return final_score, feedback[:500] # Cap feedback length
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
# -- Judge LM holder ----------------------------------------------------------
|
|
143
|
-
|
|
144
|
-
_judge_lm: dspy.LM | None = None
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
def set_judge_lm(lm) -> None:
|
|
148
|
-
"""Set the LLM used for judging. Must be called before using the metric."""
|
|
149
|
-
global _judge_lm
|
|
150
|
-
_judge_lm = lm
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
# -- Main metric function -----------------------------------------------------
|
|
154
|
-
|
|
155
|
-
def wiki_quality_metric(
|
|
156
|
-
gold: dspy.Example,
|
|
157
|
-
pred: dspy.Prediction | None,
|
|
158
|
-
trace=None,
|
|
159
|
-
pred_name: str | None = None,
|
|
160
|
-
pred_trace=None,
|
|
161
|
-
) -> dspy.Prediction:
|
|
162
|
-
"""LLM-as-judge metric for wiki output quality.
|
|
163
|
-
|
|
164
|
-
Scores on 6 dimensions with rich textual feedback.
|
|
165
|
-
Returns dspy.Prediction(score=float, feedback=str).
|
|
166
|
-
"""
|
|
167
|
-
# Handle failed prediction
|
|
168
|
-
if pred is None:
|
|
169
|
-
return dspy.Prediction(
|
|
170
|
-
score=0.0,
|
|
171
|
-
feedback="Prediction failed — no output generated."
|
|
172
|
-
)
|
|
173
|
-
|
|
174
|
-
produced_output = getattr(pred, "output", "") or ""
|
|
175
|
-
if not produced_output.strip():
|
|
176
|
-
return dspy.Prediction(
|
|
177
|
-
score=0.0,
|
|
178
|
-
feedback="Empty output — the instruction produced nothing."
|
|
179
|
-
)
|
|
180
|
-
|
|
181
|
-
task_context = getattr(gold, "task_context", "")
|
|
182
|
-
expected_output = getattr(gold, "expected_output", "")
|
|
183
|
-
|
|
184
|
-
# Determine target type from metadata or default
|
|
185
|
-
target_type = getattr(gold, "target_type", "capture")
|
|
186
|
-
target_purpose = TARGET_PURPOSES.get(target_type, TARGET_PURPOSES["capture"])
|
|
187
|
-
|
|
188
|
-
# Truncate for judge context window
|
|
189
|
-
produced_trunc = produced_output[:2000]
|
|
190
|
-
expected_trunc = expected_output[:2000]
|
|
191
|
-
context_trunc = task_context[:500]
|
|
192
|
-
|
|
193
|
-
# Build judge prompt
|
|
194
|
-
judge_prompt = JUDGE_PROMPT.format(
|
|
195
|
-
target_type=target_type,
|
|
196
|
-
target_purpose=target_purpose,
|
|
197
|
-
task_context=context_trunc,
|
|
198
|
-
produced_output=produced_trunc,
|
|
199
|
-
expected_output=expected_trunc,
|
|
200
|
-
)
|
|
201
|
-
|
|
202
|
-
# Call judge LLM
|
|
203
|
-
judge = _judge_lm or dspy.settings.lm
|
|
204
|
-
if judge is None:
|
|
205
|
-
# Fallback: basic heuristic if no judge LM available
|
|
206
|
-
return _heuristic_fallback(produced_output, expected_output)
|
|
207
|
-
|
|
208
|
-
try:
|
|
209
|
-
response = judge(
|
|
210
|
-
messages=[
|
|
211
|
-
{"role": "system", "content": JUDGE_SYSTEM},
|
|
212
|
-
{"role": "user", "content": judge_prompt},
|
|
213
|
-
],
|
|
214
|
-
temperature=0.0,
|
|
215
|
-
max_tokens=1000,
|
|
216
|
-
)
|
|
217
|
-
response_text = response[0].get("text", "") if response else ""
|
|
218
|
-
except Exception as e:
|
|
219
|
-
logger.warning("Judge LLM call failed: %s", e)
|
|
220
|
-
return _heuristic_fallback(produced_output, expected_output)
|
|
221
|
-
|
|
222
|
-
# Parse judge response
|
|
223
|
-
parsed = _parse_judge_response(response_text)
|
|
224
|
-
if not parsed:
|
|
225
|
-
logger.warning("Failed to parse judge response: %s", response_text[:200])
|
|
226
|
-
return _heuristic_fallback(produced_output, expected_output)
|
|
227
|
-
|
|
228
|
-
score, feedback = _compute_score(parsed)
|
|
229
|
-
|
|
230
|
-
return dspy.Prediction(score=score, feedback=feedback)
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
def _heuristic_fallback(output: str, expected: str) -> dspy.Prediction:
|
|
234
|
-
"""Simple heuristic scoring when judge LLM is unavailable."""
|
|
235
|
-
score = 0.0
|
|
236
|
-
parts = []
|
|
237
|
-
|
|
238
|
-
# Structure check
|
|
239
|
-
has_sections = bool(re.search(r"^## ", output, re.MULTILINE))
|
|
240
|
-
if has_sections:
|
|
241
|
-
score += 0.15
|
|
242
|
-
else:
|
|
243
|
-
parts.append("structure=0: no ## sections")
|
|
244
|
-
|
|
245
|
-
# Wikilinks check
|
|
246
|
-
has_wikilinks = "[[" in output and "]]" in output
|
|
247
|
-
if has_wikilinks:
|
|
248
|
-
score += 0.15
|
|
249
|
-
else:
|
|
250
|
-
parts.append("cross_refs=0: no [[wikilinks]]")
|
|
251
|
-
|
|
252
|
-
# Length check
|
|
253
|
-
if 200 <= len(output) <= 5000:
|
|
254
|
-
score += 0.15
|
|
255
|
-
else:
|
|
256
|
-
parts.append(f"length issue: {len(output)} chars")
|
|
257
|
-
|
|
258
|
-
# Security — no obvious secrets
|
|
259
|
-
has_secrets = bool(re.search(r"sk-[a-zA-Z0-9]{20,}|api[_-]?key\s*[:=]\s*['\"][^'\"]{10,}", output, re.IGNORECASE))
|
|
260
|
-
if not has_secrets:
|
|
261
|
-
score += 0.15
|
|
262
|
-
else:
|
|
263
|
-
parts.append("security=0: possible secret detected")
|
|
264
|
-
|
|
265
|
-
# Content overlap with expected
|
|
266
|
-
expected_words = set(expected.lower().split())
|
|
267
|
-
output_words = set(output.lower().split())
|
|
268
|
-
if expected_words:
|
|
269
|
-
overlap = len(expected_words & output_words) / len(expected_words)
|
|
270
|
-
score += overlap * 0.2
|
|
271
|
-
else:
|
|
272
|
-
score += 0.1
|
|
273
|
-
|
|
274
|
-
# Non-empty
|
|
275
|
-
if len(output) > 100:
|
|
276
|
-
score += 0.1
|
|
277
|
-
|
|
278
|
-
feedback = " | ".join(parts) if parts else "Heuristic: acceptable output"
|
|
279
|
-
return dspy.Prediction(score=min(score, 1.0), feedback=f"[heuristic] {feedback}")
|
package/scripts/gepa/optimize.py
DELETED
|
@@ -1,367 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""GEPA optimizer for pi-para prompts, tools, and skills.
|
|
3
|
-
|
|
4
|
-
Usage (via uv):
|
|
5
|
-
uv run --project scripts/gepa scripts/gepa/optimize.py \
|
|
6
|
-
--targets-file ~/.pi/wiki/gepa/input/targets.json \
|
|
7
|
-
--wiki-dir ~/.pi/wiki \
|
|
8
|
-
--output ~/.pi/wiki/gepa/output/results.json \
|
|
9
|
-
--model anthropic/claude-sonnet-4-20250514 \
|
|
10
|
-
--auto light
|
|
11
|
-
|
|
12
|
-
The TypeScript orchestrator prepares targets.json and reads results.json.
|
|
13
|
-
"""
|
|
14
|
-
|
|
15
|
-
from __future__ import annotations
|
|
16
|
-
|
|
17
|
-
import argparse
|
|
18
|
-
import json
|
|
19
|
-
import logging
|
|
20
|
-
import os
|
|
21
|
-
import sys
|
|
22
|
-
from datetime import datetime, timezone
|
|
23
|
-
from pathlib import Path
|
|
24
|
-
|
|
25
|
-
import dspy
|
|
26
|
-
|
|
27
|
-
# Local imports (same directory)
|
|
28
|
-
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
29
|
-
|
|
30
|
-
from lm_providers import create_lm
|
|
31
|
-
from program import PromptProxy, extract_evolved_instruction
|
|
32
|
-
from metric import wiki_quality_metric, set_judge_lm
|
|
33
|
-
from dataset import build_dataset, get_wiki_context
|
|
34
|
-
|
|
35
|
-
logging.basicConfig(
|
|
36
|
-
level=logging.INFO,
|
|
37
|
-
format="[gepa] %(asctime)s %(levelname)s %(message)s",
|
|
38
|
-
datefmt="%H:%M:%S",
|
|
39
|
-
)
|
|
40
|
-
logger = logging.getLogger(__name__)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
# -- Target type mapping -------------------------------------------------------
|
|
44
|
-
|
|
45
|
-
TARGET_TYPE_MAP = {
|
|
46
|
-
# Prompt templates
|
|
47
|
-
"wiki-system-prompt": "system",
|
|
48
|
-
"ingest-prompt": "ingest",
|
|
49
|
-
"query-prompt": "query",
|
|
50
|
-
"capture-system-prompt": "capture",
|
|
51
|
-
"capture-prompt": "capture",
|
|
52
|
-
"explicit-capture-prompt": "capture",
|
|
53
|
-
"summarize-system-prompt": "system",
|
|
54
|
-
"iterative-update-prompt": "ingest",
|
|
55
|
-
"overview-prompt": "query",
|
|
56
|
-
"lint-prompt": "maintenance",
|
|
57
|
-
"maintenance-system-prompt": "maintenance",
|
|
58
|
-
"processor-capture-prompt": "capture",
|
|
59
|
-
# Tool instructions
|
|
60
|
-
"tool-wiki-ingest": "tool",
|
|
61
|
-
"tool-wiki-query": "tool",
|
|
62
|
-
"tool-wiki-write": "tool",
|
|
63
|
-
"tool-wiki-read": "tool",
|
|
64
|
-
"tool-wiki-summarize": "tool",
|
|
65
|
-
# Skill instructions
|
|
66
|
-
"skill-para": "skill",
|
|
67
|
-
"skill-setup": "skill",
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
def _patch_dspy_parallelizer():
|
|
72
|
-
"""Monkey-patch DSPy's ParallelExecutor to handle exceptions properly.
|
|
73
|
-
|
|
74
|
-
DSPy bug: parallelizer returns exception objects, but Evaluate only
|
|
75
|
-
handles None, causing 'too many values to unpack' errors.
|
|
76
|
-
"""
|
|
77
|
-
try:
|
|
78
|
-
from dspy.utils import parallelizer
|
|
79
|
-
|
|
80
|
-
original_wrap = parallelizer.ParallelExecutor._wrap_function
|
|
81
|
-
|
|
82
|
-
def patched_wrap(self, user_function):
|
|
83
|
-
original_safe_func = original_wrap(self, user_function)
|
|
84
|
-
def safer_func(item):
|
|
85
|
-
result = original_safe_func(item)
|
|
86
|
-
if isinstance(result, Exception):
|
|
87
|
-
return None
|
|
88
|
-
return result
|
|
89
|
-
return safer_func
|
|
90
|
-
|
|
91
|
-
parallelizer.ParallelExecutor._wrap_function = patched_wrap
|
|
92
|
-
logger.info("Applied DSPy parallelizer exception patch")
|
|
93
|
-
except Exception as e:
|
|
94
|
-
logger.warning("Failed to patch parallelizer: %s", e)
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
def run_gepa(
|
|
98
|
-
target_name: str,
|
|
99
|
-
instruction: str,
|
|
100
|
-
target_type: str,
|
|
101
|
-
wiki_dir: str,
|
|
102
|
-
model_spec: str,
|
|
103
|
-
reflection_model_spec: str | None = None,
|
|
104
|
-
judge_model_spec: str | None = None,
|
|
105
|
-
auto: str = "light",
|
|
106
|
-
max_metric_calls: int | None = None,
|
|
107
|
-
num_threads: int = 2,
|
|
108
|
-
seed: int = 42,
|
|
109
|
-
) -> dict:
|
|
110
|
-
"""Run GEPA optimization on a single target.
|
|
111
|
-
|
|
112
|
-
Args:
|
|
113
|
-
target_name: Target identifier (e.g. 'capture-system-prompt')
|
|
114
|
-
instruction: Current instruction/prompt text
|
|
115
|
-
target_type: Type for dataset/metric (capture, ingest, query, etc.)
|
|
116
|
-
wiki_dir: Path to wiki directory
|
|
117
|
-
model_spec: Model for the proxy (e.g. 'anthropic/claude-sonnet-4-20250514')
|
|
118
|
-
reflection_model_spec: Model for GEPA reflection (defaults to model_spec)
|
|
119
|
-
auto: GEPA budget preset ('light', 'medium', 'heavy')
|
|
120
|
-
max_metric_calls: Override auto with explicit budget
|
|
121
|
-
num_threads: Parallel threads for evaluation
|
|
122
|
-
seed: Random seed
|
|
123
|
-
|
|
124
|
-
Returns:
|
|
125
|
-
Result dict with optimized instruction, scores, etc.
|
|
126
|
-
"""
|
|
127
|
-
logger.info("Optimizing target: %s (type: %s)", target_name, target_type)
|
|
128
|
-
|
|
129
|
-
# 1. Create LM instances (teacher/student/judge pattern)
|
|
130
|
-
# Student LM: runs the proxy (generates wiki output from instruction)
|
|
131
|
-
student_lm = create_lm(model_spec, temperature=0.0)
|
|
132
|
-
|
|
133
|
-
# Teacher/Reflection LM: proposes mutations (needs creativity → temperature=1.0)
|
|
134
|
-
teacher_spec = reflection_model_spec or model_spec
|
|
135
|
-
teacher_lm = create_lm(teacher_spec, temperature=1.0, max_tokens=8000)
|
|
136
|
-
|
|
137
|
-
# Judge LM: scores candidates via metric (fast + cheap)
|
|
138
|
-
judge_spec = judge_model_spec or model_spec
|
|
139
|
-
judge_lm = create_lm(judge_spec, temperature=0.0)
|
|
140
|
-
set_judge_lm(judge_lm)
|
|
141
|
-
|
|
142
|
-
logger.info("Student: %s | Teacher: %s | Judge: %s", model_spec, teacher_spec, judge_spec)
|
|
143
|
-
|
|
144
|
-
# Configure DSPy with task LM
|
|
145
|
-
dspy.configure(lm=student_lm)
|
|
146
|
-
|
|
147
|
-
# 2. Build dataset
|
|
148
|
-
logger.info("Building dataset from wiki pages...")
|
|
149
|
-
try:
|
|
150
|
-
trainset, valset = build_dataset(
|
|
151
|
-
wiki_dir=wiki_dir,
|
|
152
|
-
target_type=target_type,
|
|
153
|
-
max_examples=50,
|
|
154
|
-
val_ratio=0.4,
|
|
155
|
-
seed=seed,
|
|
156
|
-
)
|
|
157
|
-
except ValueError as e:
|
|
158
|
-
logger.error("Dataset build failed: %s", e)
|
|
159
|
-
return {
|
|
160
|
-
"target": target_name,
|
|
161
|
-
"status": "error",
|
|
162
|
-
"error": str(e),
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
logger.info("Dataset: %d train, %d val examples", len(trainset), len(valset))
|
|
166
|
-
|
|
167
|
-
# 3. Create proxy from instruction
|
|
168
|
-
proxy = PromptProxy.from_instruction(instruction)
|
|
169
|
-
|
|
170
|
-
# 4. Compute baseline score
|
|
171
|
-
logger.info("Computing baseline...")
|
|
172
|
-
baseline_scores = []
|
|
173
|
-
for ex in trainset[:min(5, len(trainset))]:
|
|
174
|
-
try:
|
|
175
|
-
pred = proxy(task_context=ex.task_context)
|
|
176
|
-
result = wiki_quality_metric(ex, pred)
|
|
177
|
-
baseline_scores.append(float(result.score))
|
|
178
|
-
except Exception as e:
|
|
179
|
-
logger.debug("Baseline eval failed: %s", e)
|
|
180
|
-
baseline_scores.append(0.0)
|
|
181
|
-
|
|
182
|
-
baseline_score = sum(baseline_scores) / len(baseline_scores) if baseline_scores else 0.0
|
|
183
|
-
logger.info("Baseline score: %.3f", baseline_score)
|
|
184
|
-
|
|
185
|
-
# 5. Patch DSPy parallelizer
|
|
186
|
-
_patch_dspy_parallelizer()
|
|
187
|
-
|
|
188
|
-
# 6. Run GEPA
|
|
189
|
-
logger.info("Starting GEPA optimization (auto=%s)...", auto)
|
|
190
|
-
|
|
191
|
-
gepa_kwargs = {
|
|
192
|
-
"metric": wiki_quality_metric,
|
|
193
|
-
"reflection_lm": teacher_lm,
|
|
194
|
-
"reflection_minibatch_size": 3,
|
|
195
|
-
"candidate_selection_strategy": "pareto",
|
|
196
|
-
"skip_perfect_score": True,
|
|
197
|
-
"use_merge": True,
|
|
198
|
-
"num_threads": num_threads,
|
|
199
|
-
"failure_score": 0.0,
|
|
200
|
-
"perfect_score": 1.0,
|
|
201
|
-
"track_stats": True,
|
|
202
|
-
"seed": seed,
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
if max_metric_calls:
|
|
206
|
-
gepa_kwargs["max_metric_calls"] = max_metric_calls
|
|
207
|
-
else:
|
|
208
|
-
gepa_kwargs["auto"] = auto
|
|
209
|
-
|
|
210
|
-
try:
|
|
211
|
-
optimizer = dspy.GEPA(**gepa_kwargs)
|
|
212
|
-
optimized_proxy = optimizer.compile(
|
|
213
|
-
student=proxy,
|
|
214
|
-
trainset=trainset,
|
|
215
|
-
valset=valset,
|
|
216
|
-
)
|
|
217
|
-
except Exception as e:
|
|
218
|
-
logger.error("GEPA optimization failed: %s", e)
|
|
219
|
-
return {
|
|
220
|
-
"target": target_name,
|
|
221
|
-
"status": "error",
|
|
222
|
-
"error": str(e),
|
|
223
|
-
"baseline_score": baseline_score,
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
# 7. Extract results
|
|
227
|
-
evolved_instruction = extract_evolved_instruction(optimized_proxy)
|
|
228
|
-
|
|
229
|
-
# Get optimized score
|
|
230
|
-
optimized_score = baseline_score
|
|
231
|
-
if hasattr(optimized_proxy, "detailed_results") and optimized_proxy.detailed_results:
|
|
232
|
-
dr = optimized_proxy.detailed_results
|
|
233
|
-
if hasattr(dr, "val_aggregate_scores") and dr.val_aggregate_scores:
|
|
234
|
-
optimized_score = max(dr.val_aggregate_scores)
|
|
235
|
-
else:
|
|
236
|
-
# Fallback: manually evaluate
|
|
237
|
-
opt_scores = []
|
|
238
|
-
for ex in trainset[:min(5, len(trainset))]:
|
|
239
|
-
try:
|
|
240
|
-
pred = optimized_proxy(task_context=ex.task_context)
|
|
241
|
-
result = wiki_quality_metric(ex, pred)
|
|
242
|
-
opt_scores.append(float(result.score))
|
|
243
|
-
except Exception:
|
|
244
|
-
opt_scores.append(0.0)
|
|
245
|
-
if opt_scores:
|
|
246
|
-
optimized_score = sum(opt_scores) / len(opt_scores)
|
|
247
|
-
|
|
248
|
-
improvement = (
|
|
249
|
-
((optimized_score - baseline_score) / baseline_score * 100)
|
|
250
|
-
if baseline_score > 0 else 0.0
|
|
251
|
-
)
|
|
252
|
-
|
|
253
|
-
logger.info(
|
|
254
|
-
"Optimization complete: %.3f → %.3f (%.1f%% improvement)",
|
|
255
|
-
baseline_score, optimized_score, improvement,
|
|
256
|
-
)
|
|
257
|
-
|
|
258
|
-
return {
|
|
259
|
-
"target": target_name,
|
|
260
|
-
"status": "success",
|
|
261
|
-
"baseline_score": round(baseline_score, 4),
|
|
262
|
-
"optimized_score": round(optimized_score, 4),
|
|
263
|
-
"improvement_pct": round(improvement, 1),
|
|
264
|
-
"optimized_instruction": evolved_instruction or instruction,
|
|
265
|
-
"original_instruction": instruction[:200] + "..." if len(instruction) > 200 else instruction,
|
|
266
|
-
"model": model_spec,
|
|
267
|
-
"auto": auto,
|
|
268
|
-
"train_size": len(trainset),
|
|
269
|
-
"val_size": len(valset),
|
|
270
|
-
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
# -- CLI entry point -----------------------------------------------------------
|
|
275
|
-
|
|
276
|
-
def main():
|
|
277
|
-
parser = argparse.ArgumentParser(description="GEPA optimizer for pi-para")
|
|
278
|
-
parser.add_argument("--targets-file", required=True, help="JSON file with targets to optimize")
|
|
279
|
-
parser.add_argument("--wiki-dir", default=os.path.expanduser("~/.pi/wiki"), help="Wiki directory")
|
|
280
|
-
parser.add_argument("--output", required=True, help="Output JSON file for results")
|
|
281
|
-
parser.add_argument("--model", default="anthropic/claude-sonnet-4-20250514", help="Student model (runs proxy)")
|
|
282
|
-
parser.add_argument("--reflection-model", default="anthropic/claude-opus-4-6", help="Teacher/reflection model (proposes mutations)")
|
|
283
|
-
parser.add_argument("--judge-model", default=None, help="Judge model (scores output, defaults to --model)")
|
|
284
|
-
parser.add_argument("--auto", default="light", choices=["light", "medium", "heavy"], help="GEPA budget")
|
|
285
|
-
parser.add_argument("--max-metric-calls", type=int, default=None, help="Override auto with explicit budget")
|
|
286
|
-
parser.add_argument("--threads", type=int, default=2, help="Parallel eval threads")
|
|
287
|
-
parser.add_argument("--seed", type=int, default=42, help="Random seed")
|
|
288
|
-
parser.add_argument("--target", default=None, help="Optimize only this target (by name)")
|
|
289
|
-
|
|
290
|
-
args = parser.parse_args()
|
|
291
|
-
|
|
292
|
-
# Load targets
|
|
293
|
-
with open(args.targets_file, "r") as f:
|
|
294
|
-
targets_data = json.load(f)
|
|
295
|
-
|
|
296
|
-
targets = targets_data.get("targets", [])
|
|
297
|
-
if not targets:
|
|
298
|
-
logger.error("No targets in %s", args.targets_file)
|
|
299
|
-
sys.exit(1)
|
|
300
|
-
|
|
301
|
-
# Filter to specific target if requested
|
|
302
|
-
if args.target:
|
|
303
|
-
targets = [t for t in targets if t["name"] == args.target]
|
|
304
|
-
if not targets:
|
|
305
|
-
logger.error("Target '%s' not found", args.target)
|
|
306
|
-
sys.exit(1)
|
|
307
|
-
|
|
308
|
-
logger.info("Optimizing %d target(s) with model %s, auto=%s", len(targets), args.model, args.auto)
|
|
309
|
-
|
|
310
|
-
# Run GEPA for each target
|
|
311
|
-
results = []
|
|
312
|
-
for target in targets:
|
|
313
|
-
name = target["name"]
|
|
314
|
-
instruction = target["content"]
|
|
315
|
-
target_type = TARGET_TYPE_MAP.get(name, "capture")
|
|
316
|
-
|
|
317
|
-
result = run_gepa(
|
|
318
|
-
target_name=name,
|
|
319
|
-
instruction=instruction,
|
|
320
|
-
target_type=target_type,
|
|
321
|
-
wiki_dir=args.wiki_dir,
|
|
322
|
-
model_spec=args.model,
|
|
323
|
-
reflection_model_spec=args.reflection_model,
|
|
324
|
-
judge_model_spec=args.judge_model,
|
|
325
|
-
auto=args.auto,
|
|
326
|
-
max_metric_calls=args.max_metric_calls,
|
|
327
|
-
num_threads=args.threads,
|
|
328
|
-
seed=args.seed,
|
|
329
|
-
)
|
|
330
|
-
results.append(result)
|
|
331
|
-
|
|
332
|
-
# Write output
|
|
333
|
-
output_dir = os.path.dirname(args.output)
|
|
334
|
-
if output_dir:
|
|
335
|
-
os.makedirs(output_dir, exist_ok=True)
|
|
336
|
-
|
|
337
|
-
output_data = {
|
|
338
|
-
"results": results,
|
|
339
|
-
"model": args.model,
|
|
340
|
-
"auto": args.auto,
|
|
341
|
-
"wiki_dir": args.wiki_dir,
|
|
342
|
-
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
with open(args.output, "w") as f:
|
|
346
|
-
json.dump(output_data, f, indent=2)
|
|
347
|
-
|
|
348
|
-
logger.info("Results written to %s", args.output)
|
|
349
|
-
|
|
350
|
-
# Print summary
|
|
351
|
-
print("\n" + "=" * 60)
|
|
352
|
-
print("GEPA Optimization Summary")
|
|
353
|
-
print("=" * 60)
|
|
354
|
-
for r in results:
|
|
355
|
-
status = r.get("status", "unknown")
|
|
356
|
-
if status == "success":
|
|
357
|
-
print(
|
|
358
|
-
f" {r['target']}: {r['baseline_score']:.3f} → {r['optimized_score']:.3f} "
|
|
359
|
-
f"({r['improvement_pct']:+.1f}%)"
|
|
360
|
-
)
|
|
361
|
-
else:
|
|
362
|
-
print(f" {r['target']}: ERROR — {r.get('error', 'unknown')}")
|
|
363
|
-
print("=" * 60)
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
if __name__ == "__main__":
|
|
367
|
-
main()
|