pi-para 0.6.0 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/ARCHITECTURE.md +0 -9
  2. package/CHANGELOG.md +22 -0
  3. package/CONFIGURATION.md +5 -23
  4. package/README.md +7 -31
  5. package/SETUP.md +5 -3
  6. package/STRUCTURE.md +0 -9
  7. package/dist/atomic-write.d.ts +2 -0
  8. package/dist/atomic-write.d.ts.map +1 -0
  9. package/dist/atomic-write.js +18 -0
  10. package/dist/atomic-write.js.map +1 -0
  11. package/dist/capture.d.ts +34 -0
  12. package/dist/capture.d.ts.map +1 -0
  13. package/dist/capture.js +461 -0
  14. package/dist/capture.js.map +1 -0
  15. package/dist/cli-legacy.d.ts +4 -0
  16. package/dist/cli-legacy.d.ts.map +1 -0
  17. package/dist/cli-legacy.js +41 -0
  18. package/dist/cli-legacy.js.map +1 -0
  19. package/dist/cli.d.ts +8 -0
  20. package/dist/cli.d.ts.map +1 -0
  21. package/dist/cli.js +427 -0
  22. package/dist/cli.js.map +1 -0
  23. package/dist/commands.d.ts +15 -0
  24. package/dist/commands.d.ts.map +1 -0
  25. package/dist/commands.js +791 -0
  26. package/dist/commands.js.map +1 -0
  27. package/dist/config.d.ts +94 -0
  28. package/dist/config.d.ts.map +1 -0
  29. package/dist/config.js +228 -0
  30. package/dist/config.js.map +1 -0
  31. package/dist/context.d.ts +63 -0
  32. package/dist/context.d.ts.map +1 -0
  33. package/dist/context.js +311 -0
  34. package/dist/context.js.map +1 -0
  35. package/dist/credentials.d.ts +27 -0
  36. package/dist/credentials.d.ts.map +1 -0
  37. package/dist/credentials.js +85 -0
  38. package/dist/credentials.js.map +1 -0
  39. package/dist/daemon.d.ts +49 -0
  40. package/dist/daemon.d.ts.map +1 -0
  41. package/dist/daemon.js +226 -0
  42. package/dist/daemon.js.map +1 -0
  43. package/dist/doctor.d.ts +20 -0
  44. package/dist/doctor.d.ts.map +1 -0
  45. package/dist/doctor.js +197 -0
  46. package/dist/doctor.js.map +1 -0
  47. package/dist/frontmatter.d.ts +59 -0
  48. package/dist/frontmatter.d.ts.map +1 -0
  49. package/dist/frontmatter.js +170 -0
  50. package/dist/frontmatter.js.map +1 -0
  51. package/dist/index.d.ts +21 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +357 -0
  54. package/dist/index.js.map +1 -0
  55. package/dist/ingest.d.ts +88 -0
  56. package/dist/ingest.d.ts.map +1 -0
  57. package/dist/ingest.js +105 -0
  58. package/dist/ingest.js.map +1 -0
  59. package/dist/link-discovery.d.ts +40 -0
  60. package/dist/link-discovery.d.ts.map +1 -0
  61. package/dist/link-discovery.js +122 -0
  62. package/dist/link-discovery.js.map +1 -0
  63. package/dist/link-utils.d.ts +30 -0
  64. package/dist/link-utils.d.ts.map +1 -0
  65. package/dist/link-utils.js +145 -0
  66. package/dist/link-utils.js.map +1 -0
  67. package/dist/lint.d.ts +39 -0
  68. package/dist/lint.d.ts.map +1 -0
  69. package/dist/lint.js +827 -0
  70. package/dist/lint.js.map +1 -0
  71. package/dist/maintainer.d.ts +24 -0
  72. package/dist/maintainer.d.ts.map +1 -0
  73. package/dist/maintainer.js +358 -0
  74. package/dist/maintainer.js.map +1 -0
  75. package/dist/model-resolver.d.ts +30 -0
  76. package/dist/model-resolver.d.ts.map +1 -0
  77. package/dist/model-resolver.js +71 -0
  78. package/dist/model-resolver.js.map +1 -0
  79. package/dist/paths.d.ts +20 -0
  80. package/dist/paths.d.ts.map +1 -0
  81. package/dist/paths.js +33 -0
  82. package/dist/paths.js.map +1 -0
  83. package/dist/processor.d.ts +14 -0
  84. package/dist/processor.d.ts.map +1 -0
  85. package/dist/processor.js +361 -0
  86. package/dist/processor.js.map +1 -0
  87. package/dist/qmd-providers.d.ts +12 -0
  88. package/dist/qmd-providers.d.ts.map +1 -0
  89. package/dist/qmd-providers.js +45 -0
  90. package/dist/qmd-providers.js.map +1 -0
  91. package/dist/query.d.ts +69 -0
  92. package/dist/query.d.ts.map +1 -0
  93. package/dist/query.js +120 -0
  94. package/dist/query.js.map +1 -0
  95. package/dist/raw.d.ts +57 -0
  96. package/dist/raw.d.ts.map +1 -0
  97. package/dist/raw.js +219 -0
  98. package/dist/raw.js.map +1 -0
  99. package/dist/redact.d.ts +21 -0
  100. package/dist/redact.d.ts.map +1 -0
  101. package/dist/redact.js +143 -0
  102. package/dist/redact.js.map +1 -0
  103. package/dist/repair.d.ts +11 -0
  104. package/dist/repair.d.ts.map +1 -0
  105. package/dist/repair.js +41 -0
  106. package/dist/repair.js.map +1 -0
  107. package/dist/scheduler/controls.d.ts +35 -0
  108. package/dist/scheduler/controls.d.ts.map +1 -0
  109. package/dist/scheduler/controls.js +98 -0
  110. package/dist/scheduler/controls.js.map +1 -0
  111. package/dist/scheduler/index.d.ts +70 -0
  112. package/dist/scheduler/index.d.ts.map +1 -0
  113. package/dist/scheduler/index.js +245 -0
  114. package/dist/scheduler/index.js.map +1 -0
  115. package/dist/scheduler/leases.d.ts +22 -0
  116. package/dist/scheduler/leases.d.ts.map +1 -0
  117. package/dist/scheduler/leases.js +74 -0
  118. package/dist/scheduler/leases.js.map +1 -0
  119. package/dist/scheduler/session-capture.d.ts +33 -0
  120. package/dist/scheduler/session-capture.d.ts.map +1 -0
  121. package/dist/scheduler/session-capture.js +108 -0
  122. package/dist/scheduler/session-capture.js.map +1 -0
  123. package/dist/scheduler/state.d.ts +56 -0
  124. package/dist/scheduler/state.d.ts.map +1 -0
  125. package/dist/scheduler/state.js +210 -0
  126. package/dist/scheduler/state.js.map +1 -0
  127. package/dist/scope.d.ts +54 -0
  128. package/dist/scope.d.ts.map +1 -0
  129. package/dist/scope.js +161 -0
  130. package/dist/scope.js.map +1 -0
  131. package/dist/scripts/bulk-link-fix.d.ts +9 -0
  132. package/dist/scripts/bulk-link-fix.d.ts.map +1 -0
  133. package/dist/scripts/bulk-link-fix.js +106 -0
  134. package/dist/scripts/bulk-link-fix.js.map +1 -0
  135. package/dist/session-tools.d.ts +33 -0
  136. package/dist/session-tools.d.ts.map +1 -0
  137. package/dist/session-tools.js +211 -0
  138. package/dist/session-tools.js.map +1 -0
  139. package/dist/settings.d.ts +37 -0
  140. package/dist/settings.d.ts.map +1 -0
  141. package/dist/settings.js +176 -0
  142. package/dist/settings.js.map +1 -0
  143. package/dist/setup.d.ts +29 -0
  144. package/dist/setup.d.ts.map +1 -0
  145. package/dist/setup.js +90 -0
  146. package/dist/setup.js.map +1 -0
  147. package/dist/state.d.ts +54 -0
  148. package/dist/state.d.ts.map +1 -0
  149. package/dist/state.js +176 -0
  150. package/dist/state.js.map +1 -0
  151. package/dist/status.d.ts +22 -0
  152. package/dist/status.d.ts.map +1 -0
  153. package/dist/status.js +93 -0
  154. package/dist/status.js.map +1 -0
  155. package/dist/store.d.ts +71 -0
  156. package/dist/store.d.ts.map +1 -0
  157. package/dist/store.js +282 -0
  158. package/dist/store.js.map +1 -0
  159. package/dist/summarize.d.ts +60 -0
  160. package/dist/summarize.d.ts.map +1 -0
  161. package/dist/summarize.js +266 -0
  162. package/dist/summarize.js.map +1 -0
  163. package/dist/tag-registry.d.ts +56 -0
  164. package/dist/tag-registry.d.ts.map +1 -0
  165. package/dist/tag-registry.js +146 -0
  166. package/dist/tag-registry.js.map +1 -0
  167. package/dist/templates/prompts.d.ts +51 -0
  168. package/dist/templates/prompts.d.ts.map +1 -0
  169. package/dist/templates/prompts.js +203 -0
  170. package/dist/templates/prompts.js.map +1 -0
  171. package/dist/tools.d.ts +26 -0
  172. package/dist/tools.d.ts.map +1 -0
  173. package/dist/tools.js +1198 -0
  174. package/dist/tools.js.map +1 -0
  175. package/dist/watcher.d.ts +25 -0
  176. package/dist/watcher.d.ts.map +1 -0
  177. package/dist/watcher.js +84 -0
  178. package/dist/watcher.js.map +1 -0
  179. package/dist/webui/client/dist/index.html +1885 -0
  180. package/dist/webui/server.d.ts +31 -0
  181. package/dist/webui/server.d.ts.map +1 -0
  182. package/dist/webui/server.js +566 -0
  183. package/dist/webui/server.js.map +1 -0
  184. package/dist/wiki-tool-guidance.d.ts +6 -0
  185. package/dist/wiki-tool-guidance.d.ts.map +1 -0
  186. package/dist/wiki-tool-guidance.js +70 -0
  187. package/dist/wiki-tool-guidance.js.map +1 -0
  188. package/dist/wiki.d.ts +108 -0
  189. package/dist/wiki.d.ts.map +1 -0
  190. package/dist/wiki.js +497 -0
  191. package/dist/wiki.js.map +1 -0
  192. package/package.json +2 -1
  193. package/scripts/build-assets.mjs +1 -13
  194. package/skills/setup/SKILL.md +1 -1
  195. package/src/cli.ts +0 -70
  196. package/src/commands.ts +78 -3
  197. package/src/config.ts +0 -25
  198. package/src/doctor.ts +21 -1
  199. package/src/index.ts +16 -38
  200. package/src/scheduler/index.ts +64 -0
  201. package/src/settings.ts +88 -0
  202. package/src/setup.ts +1 -1
  203. package/src/templates/prompts.ts +0 -56
  204. package/scripts/gepa/dataset.py +0 -192
  205. package/scripts/gepa/lm_providers.py +0 -472
  206. package/scripts/gepa/metric.py +0 -279
  207. package/scripts/gepa/optimize.py +0 -367
  208. package/scripts/gepa/program.py +0 -99
  209. package/scripts/gepa/pyproject.toml +0 -19
  210. package/scripts/gepa/run-all.sh +0 -53
  211. package/scripts/gepa/wiki_reader.py +0 -113
  212. package/src/gepa/index.ts +0 -287
  213. package/src/gepa/optimized/capture-prompt.txt +0 -65
@@ -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}")
@@ -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()