pi-simocracy 0.2.0 → 0.3.0

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.
@@ -1,131 +0,0 @@
1
- /**
2
- * `/sim train chat` — adaptive interview turn loop.
3
- *
4
- * Mirrors `/api/training/next-question` from simocracy-v2 but calls
5
- * OpenRouter directly (no pi loop / persona injection). End the chat
6
- * by typing `/done` (or cancelling the prompt).
7
- */
8
-
9
- import type { ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
10
-
11
- import type { LoadedSim } from "../persona.ts";
12
- import { openRouterComplete, TRAINING_CHAT_MODEL } from "../openrouter.ts";
13
- import {
14
- loadTrainingLabState,
15
- saveTrainingLabState,
16
- } from "./storage.ts";
17
- import {
18
- clampConstitution,
19
- clampTranscript,
20
- renderBaselineForPrompt,
21
- wrapAsData,
22
- } from "./prompt-helpers.ts";
23
- import { buildNextQuestionSystemPrompt } from "./prompts.ts";
24
- import type { InterviewTurn } from "./types.ts";
25
-
26
- export async function runChat(
27
- ctx: ExtensionCommandContext,
28
- loadedSim: LoadedSim,
29
- ): Promise<void> {
30
- let state = loadTrainingLabState(loadedSim.rkey);
31
- const proposals = state.questionSet?.proposals ?? [];
32
-
33
- if (proposals.length === 0) {
34
- ctx.ui.notify(
35
- "Run `/sim train baseline` first — chat needs a baseline questionnaire to ground its questions.",
36
- "warning",
37
- );
38
- return;
39
- }
40
-
41
- ctx.ui.notify(
42
- `Chatting with ${loadedSim.name}. Type /done to end the conversation.`,
43
- "info",
44
- );
45
-
46
- // Always print the existing transcript so the user has context.
47
- if (state.interviewTurns.length > 0) {
48
- ctx.ui.notify(
49
- `Loaded ${state.interviewTurns.length} prior turns from disk.`,
50
- "info",
51
- );
52
- }
53
-
54
- // Prime the assistant with the first question if the transcript is empty.
55
- if (state.interviewTurns.length === 0) {
56
- const reply = await callNextQuestion(loadedSim, state.interviewTurns, state, proposals);
57
- if (reply) {
58
- console.log(`\n${loadedSim.name}: ${reply}\n`);
59
- state = appendTurn(state, { role: "assistant", content: reply });
60
- saveTrainingLabState(loadedSim.rkey, state);
61
- }
62
- }
63
-
64
- for (;;) {
65
- const userMessage = await ctx.ui.input(
66
- `Reply to ${loadedSim.name} (or /done to finish)`,
67
- "Your reply",
68
- );
69
- if (userMessage === undefined) break;
70
- const trimmed = userMessage.trim();
71
- if (!trimmed) continue;
72
- if (trimmed === "/done" || trimmed === "/exit" || trimmed === "/quit") break;
73
-
74
- state = appendTurn(state, { role: "user", content: trimmed });
75
- saveTrainingLabState(loadedSim.rkey, state);
76
-
77
- let reply: string;
78
- try {
79
- reply = await callNextQuestion(loadedSim, state.interviewTurns, state, proposals);
80
- } catch (err) {
81
- ctx.ui.notify(`OpenRouter error: ${(err as Error).message}`, "error");
82
- break;
83
- }
84
- if (!reply) {
85
- ctx.ui.notify("Empty model response. Try again or /done.", "warning");
86
- continue;
87
- }
88
- console.log(`\n${loadedSim.name}: ${reply}\n`);
89
- state = appendTurn(state, { role: "assistant", content: reply });
90
- saveTrainingLabState(loadedSim.rkey, state);
91
- }
92
-
93
- ctx.ui.notify(
94
- `Saved ${state.interviewTurns.length} turns. Run \`/sim train profile\` to distill.`,
95
- "info",
96
- );
97
- }
98
-
99
- async function callNextQuestion(
100
- loadedSim: LoadedSim,
101
- transcript: InterviewTurn[],
102
- state: ReturnType<typeof loadTrainingLabState>,
103
- proposals: { id: string; title: string; summary: string; topic: string }[],
104
- ): Promise<string> {
105
- const system = buildNextQuestionSystemPrompt(loadedSim.name, loadedSim.style);
106
- const constitution = clampConstitution(loadedSim.description);
107
- const baseline = renderBaselineForPrompt(state.baselineVotes, proposals);
108
- const clamped = clampTranscript(transcript);
109
-
110
- const userPrompt = [
111
- `simName: ${loadedSim.name}`,
112
- wrapAsData("existingConstitution", constitution),
113
- wrapAsData("baselineQuestionnaire", baseline),
114
- wrapAsData("transcript", JSON.stringify(clamped, null, 2)),
115
- ].join("\n\n");
116
-
117
- return openRouterComplete(
118
- [
119
- { role: "system", content: system },
120
- { role: "user", content: userPrompt },
121
- ],
122
- { model: TRAINING_CHAT_MODEL, maxTokens: 600, temperature: 0.6 },
123
- );
124
- }
125
-
126
- function appendTurn<T extends { interviewTurns: InterviewTurn[] }>(
127
- state: T,
128
- turn: InterviewTurn,
129
- ): T {
130
- return { ...state, interviewTurns: [...state.interviewTurns, turn] };
131
- }
@@ -1,81 +0,0 @@
1
- /**
2
- * `/sim train feedback` — free-form chat with the loaded sim about
3
- * its constitution. Same pattern as `/sim train chat` but uses the
4
- * regular sim persona prompt (`buildSimPrompt`) — i.e., the sim
5
- * speaks in character — so the user gets real feedback on how the
6
- * sim sounds, then can use the transcript to inform an Apply step.
7
- *
8
- * Web parity: `simocracy-v2/components/sim/training-lab/feedback-tab.tsx`.
9
- */
10
-
11
- import type { ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
12
-
13
- import { buildSimPrompt, type LoadedSim } from "../persona.ts";
14
- import { openRouterComplete, TRAINING_CHAT_MODEL } from "../openrouter.ts";
15
- import {
16
- loadTrainingLabState,
17
- saveTrainingLabState,
18
- } from "./storage.ts";
19
- import type { FeedbackTurn } from "./types.ts";
20
-
21
- export async function runFeedback(
22
- ctx: ExtensionCommandContext,
23
- loadedSim: LoadedSim,
24
- ): Promise<void> {
25
- let state = loadTrainingLabState(loadedSim.rkey);
26
- let turns: FeedbackTurn[] = state.feedbackTurns ?? [];
27
-
28
- ctx.ui.notify(
29
- `Free-form chat with ${loadedSim.name}. Type /done to end. Transcript persists for the Apply step.`,
30
- "info",
31
- );
32
- if (turns.length > 0) {
33
- ctx.ui.notify(`Loaded ${turns.length} prior feedback turns from disk.`, "info");
34
- }
35
-
36
- for (;;) {
37
- const userMessage = await ctx.ui.input(
38
- `Ask ${loadedSim.name} (or /done to finish)`,
39
- "Your message",
40
- );
41
- if (userMessage === undefined) break;
42
- const trimmed = userMessage.trim();
43
- if (!trimmed) continue;
44
- if (trimmed === "/done" || trimmed === "/exit" || trimmed === "/quit") break;
45
-
46
- turns = [...turns, { role: "user", content: trimmed }];
47
- state = { ...state, feedbackTurns: turns };
48
- saveTrainingLabState(loadedSim.rkey, state);
49
-
50
- let reply: string;
51
- try {
52
- reply = await callSimChat(loadedSim, turns);
53
- } catch (err) {
54
- ctx.ui.notify(`OpenRouter error: ${(err as Error).message}`, "error");
55
- break;
56
- }
57
- if (!reply) {
58
- ctx.ui.notify("Empty model response. Try again or /done.", "warning");
59
- continue;
60
- }
61
- console.log(`\n${loadedSim.name}: ${reply}\n`);
62
- turns = [...turns, { role: "assistant", content: reply }];
63
- state = { ...state, feedbackTurns: turns };
64
- saveTrainingLabState(loadedSim.rkey, state);
65
- }
66
-
67
- ctx.ui.notify(`Saved ${turns.length} feedback turns.`, "info");
68
- }
69
-
70
- async function callSimChat(loadedSim: LoadedSim, turns: FeedbackTurn[]): Promise<string> {
71
- const systemPrompt = buildSimPrompt(loadedSim);
72
- const messages = [
73
- { role: "system" as const, content: systemPrompt },
74
- ...turns.map((t) => ({ role: t.role, content: t.content })),
75
- ];
76
- return openRouterComplete(messages, {
77
- model: TRAINING_CHAT_MODEL,
78
- maxTokens: 600,
79
- temperature: 0.7,
80
- });
81
- }
@@ -1,142 +0,0 @@
1
- /**
2
- * `/sim train …` command dispatcher.
3
- *
4
- * Sub-commands mirror the five tabs of the Training Lab in
5
- * simocracy-v2 plus `status` / `reset`. Run `/sim train` (no arg) for
6
- * usage. All flows operate on the currently-loaded sim and persist
7
- * state to `~/.local/share/pi-simocracy/training/<rkey>.json`.
8
- */
9
-
10
- import type { ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
11
-
12
- import type { LoadedSim } from "../persona.ts";
13
- import { runBaseline } from "./baseline.ts";
14
- import { runChat } from "./chat.ts";
15
- import { runProfile } from "./profile.ts";
16
- import { runFeedback } from "./feedback.ts";
17
- import { runAlignment } from "./alignment.ts";
18
- import { runApply } from "./apply.ts";
19
- import {
20
- clearTrainingLabState,
21
- loadTrainingLabState,
22
- trainingFilePath,
23
- } from "./storage.ts";
24
-
25
- const HELP = [
26
- "Usage: /sim train <subcommand>",
27
- "",
28
- " baseline Vote yes/no/abstain on the loaded sim's baseline proposals.",
29
- " chat Adaptive interview with the sim — fill gaps in your stance.",
30
- " profile Distill baseline + transcript into a TrainingProfile.",
31
- " feedback Free-form chat with the sim about its constitution.",
32
- " alignment Score the sim against your hidden baseline votes.",
33
- " apply Merge the profile into the constitution (clipboard copy in PR 2).",
34
- " status Print local state path + counts.",
35
- " reset Delete local training data for this sim.",
36
- ].join("\n");
37
-
38
- export async function runTrainCommand(
39
- ctx: ExtensionCommandContext,
40
- arg: string,
41
- loadedSim: LoadedSim | null,
42
- ): Promise<void> {
43
- const tokens = arg.trim().split(/\s+/).filter(Boolean);
44
- const sub = tokens[0] ?? "";
45
- const flags = new Set(tokens.slice(1));
46
-
47
- if (!sub || sub === "help" || sub === "--help") {
48
- ctx.ui.notify(HELP, "info");
49
- return;
50
- }
51
-
52
- if (sub === "status") {
53
- await runStatus(ctx, loadedSim);
54
- return;
55
- }
56
- if (sub === "reset") {
57
- await runReset(ctx, loadedSim);
58
- return;
59
- }
60
-
61
- if (!loadedSim) {
62
- ctx.ui.notify(
63
- "No sim loaded. Run `/sim <name>` first, then `/sim train …`.",
64
- "error",
65
- );
66
- return;
67
- }
68
-
69
- switch (sub) {
70
- case "baseline":
71
- await runBaseline(ctx, loadedSim);
72
- return;
73
- case "chat":
74
- await runChat(ctx, loadedSim);
75
- return;
76
- case "profile":
77
- await runProfile(ctx, loadedSim);
78
- return;
79
- case "feedback":
80
- await runFeedback(ctx, loadedSim);
81
- return;
82
- case "alignment":
83
- await runAlignment(ctx, loadedSim);
84
- return;
85
- case "apply":
86
- await runApply(ctx, loadedSim, { apply: flags.has("--apply") });
87
- return;
88
- default:
89
- ctx.ui.notify(`Unknown subcommand: ${sub}\n\n${HELP}`, "error");
90
- return;
91
- }
92
- }
93
-
94
- async function runStatus(
95
- ctx: ExtensionCommandContext,
96
- loadedSim: LoadedSim | null,
97
- ): Promise<void> {
98
- if (!loadedSim) {
99
- ctx.ui.notify("No sim loaded.", "info");
100
- return;
101
- }
102
- const path = trainingFilePath(loadedSim.rkey);
103
- const state = loadTrainingLabState(loadedSim.rkey);
104
- const cast = state.baselineVotes.filter(
105
- (v) => !(v.vote === "abstain" && Math.abs(v.importance - 0.5) < 0.01 && !v.reasoning),
106
- ).length;
107
- const lines = [
108
- `Sim: ${loadedSim.name} (${loadedSim.uri})`,
109
- `State file: ${path}`,
110
- `Updated: ${state.updatedAt}`,
111
- `Baseline: ${cast}/${state.baselineVotes.length} cast votes`,
112
- `Interview: ${state.interviewTurns.length} turns`,
113
- `Feedback: ${(state.feedbackTurns ?? []).length} turns`,
114
- `Profile: ${state.profile ? "yes" : "no"}`,
115
- `Alignment: ${state.alignment ? `${state.alignment.matchedCount}/${state.alignment.totalCount}` : "no"}`,
116
- `Question set: ${state.questionSet?.source === "template" ? state.questionSet.templateName : "(none)"}`,
117
- ];
118
- console.log("\n" + lines.join("\n") + "\n");
119
- }
120
-
121
- async function runReset(
122
- ctx: ExtensionCommandContext,
123
- loadedSim: LoadedSim | null,
124
- ): Promise<void> {
125
- if (!loadedSim) {
126
- ctx.ui.notify("No sim loaded.", "info");
127
- return;
128
- }
129
- const confirmed = await ctx.ui.confirm(
130
- "Reset training data?",
131
- `Delete all local training data for ${loadedSim.name}?`,
132
- );
133
- if (!confirmed) {
134
- ctx.ui.notify("Cancelled.", "info");
135
- return;
136
- }
137
- const removed = clearTrainingLabState(loadedSim.rkey);
138
- ctx.ui.notify(
139
- removed ? `Training data reset for ${loadedSim.name}.` : "No training data on disk.",
140
- "info",
141
- );
142
- }
@@ -1,229 +0,0 @@
1
- /**
2
- * `/sim train profile` — distill baseline + transcript into a
3
- * `TrainingProfile`. Mirrors `/api/training/extract-profile` from
4
- * simocracy-v2.
5
- */
6
-
7
- import type { ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
8
-
9
- import type { LoadedSim } from "../persona.ts";
10
- import { openRouterComplete, TRAINING_CHAT_MODEL } from "../openrouter.ts";
11
- import {
12
- loadTrainingLabState,
13
- saveTrainingLabState,
14
- } from "./storage.ts";
15
- import {
16
- clampConstitution,
17
- clampTranscript,
18
- renderBaselineForPrompt,
19
- wrapAsData,
20
- } from "./prompt-helpers.ts";
21
- import { TRAINING_EXTRACT_PROFILE_SYSTEM_PROMPT } from "./prompts.ts";
22
- import type { IssuePriority, TrainingProfile } from "./types.ts";
23
-
24
- export async function runProfile(
25
- ctx: ExtensionCommandContext,
26
- loadedSim: LoadedSim,
27
- ): Promise<TrainingProfile | null> {
28
- let state = loadTrainingLabState(loadedSim.rkey);
29
- const proposals = state.questionSet?.proposals ?? [];
30
- if (state.baselineVotes.length === 0 && state.interviewTurns.length === 0) {
31
- ctx.ui.notify(
32
- "Need at least some baseline votes or interview turns before distilling. Run `/sim train baseline` first.",
33
- "warning",
34
- );
35
- return null;
36
- }
37
-
38
- ctx.ui.notify("Distilling profile…", "info");
39
- let profile: TrainingProfile | null;
40
- try {
41
- profile = await deriveProfile(loadedSim, state, proposals);
42
- } catch (err) {
43
- ctx.ui.notify(`OpenRouter error: ${(err as Error).message}`, "error");
44
- return null;
45
- }
46
-
47
- if (!profile) {
48
- ctx.ui.notify("Could not parse profile from model response.", "error");
49
- return null;
50
- }
51
-
52
- state = { ...state, profile, alignment: null };
53
- saveTrainingLabState(loadedSim.rkey, state);
54
- printProfile(profile);
55
- ctx.ui.notify(
56
- "Profile saved. Run `/sim train alignment` next, or `/sim train apply` to merge it into the constitution.",
57
- "info",
58
- );
59
- return profile;
60
- }
61
-
62
- export async function deriveProfile(
63
- loadedSim: LoadedSim,
64
- state: ReturnType<typeof loadTrainingLabState>,
65
- proposals: { id: string; title: string; summary: string; topic: string }[],
66
- ): Promise<TrainingProfile | null> {
67
- const constitution = clampConstitution(loadedSim.description);
68
- const baseline = renderBaselineForPrompt(state.baselineVotes, proposals);
69
- const transcript = clampTranscript(state.interviewTurns);
70
-
71
- const userPrompt = [
72
- `simName: ${loadedSim.name}`,
73
- wrapAsData("existingConstitution", constitution),
74
- wrapAsData("baselineQuestionnaire", baseline),
75
- wrapAsData("transcript", JSON.stringify(transcript, null, 2)),
76
- ].join("\n\n");
77
-
78
- const content = await openRouterComplete(
79
- [
80
- { role: "system", content: TRAINING_EXTRACT_PROFILE_SYSTEM_PROMPT },
81
- { role: "user", content: userPrompt },
82
- ],
83
- { model: TRAINING_CHAT_MODEL, maxTokens: 1800, temperature: 0.3 },
84
- );
85
-
86
- return parseProfile(content);
87
- }
88
-
89
- // -----------------------------------------------------------------
90
- // Parsing — mirrors `normalizeProfile` in
91
- // `simocracy-v2/app/api/training/extract-profile/route.ts`.
92
- // -----------------------------------------------------------------
93
-
94
- function parseProfile(content: string): TrainingProfile | null {
95
- const parsed = parseJsonObject(content);
96
- if (!parsed) return null;
97
-
98
- const coreValues = normalizeStringArray(parsed.coreValues, 7);
99
- const issuePriorities = normalizeIssuePriorities(parsed.issuePriorities);
100
- const redLines = normalizeStringArray(parsed.redLines, 6);
101
- const acceptableTradeoffs = normalizeStringArray(parsed.acceptableTradeoffs, 6);
102
- const uncertaintyAreas = normalizeStringArray(parsed.uncertaintyAreas, 6);
103
- const representationRules = normalizeStringArray(parsed.representationRules, 5);
104
-
105
- if (
106
- typeof parsed.summary !== "string" ||
107
- coreValues.length < 3 ||
108
- issuePriorities.length < 4
109
- ) {
110
- return null;
111
- }
112
-
113
- return {
114
- summary: parsed.summary.trim(),
115
- coreValues,
116
- issuePriorities,
117
- redLines,
118
- acceptableTradeoffs,
119
- uncertaintyAreas,
120
- representationRules,
121
- };
122
- }
123
-
124
- function parseJsonObject(content: string): Record<string, unknown> | null {
125
- const trimmed = content.trim();
126
- try {
127
- const parsed: unknown = JSON.parse(trimmed);
128
- return isRecord(parsed) ? parsed : null;
129
- } catch {
130
- const start = trimmed.indexOf("{");
131
- const end = trimmed.lastIndexOf("}");
132
- if (start === -1 || end === -1 || end <= start) return null;
133
- try {
134
- const parsed: unknown = JSON.parse(trimmed.slice(start, end + 1));
135
- return isRecord(parsed) ? parsed : null;
136
- } catch {
137
- return null;
138
- }
139
- }
140
- }
141
-
142
- function isRecord(value: unknown): value is Record<string, unknown> {
143
- return typeof value === "object" && value !== null && !Array.isArray(value);
144
- }
145
-
146
- function normalizeStringArray(value: unknown, max: number): string[] {
147
- if (!Array.isArray(value)) return [];
148
- return value
149
- .filter((item): item is string => typeof item === "string")
150
- .map((item) => item.trim())
151
- .filter(Boolean)
152
- .slice(0, max);
153
- }
154
-
155
- function normalizeIssuePriorities(value: unknown): IssuePriority[] {
156
- if (!Array.isArray(value)) return [];
157
- return value
158
- .map((item) => {
159
- if (!isRecord(item)) return null;
160
- if (typeof item.issue !== "string" || typeof item.stance !== "string") return null;
161
- const importance = toUnit(item.importance);
162
- const negotiability = toUnit(item.negotiability);
163
- const confidence = toUnit(item.confidence);
164
- if (importance === null || negotiability === null || confidence === null) return null;
165
- return {
166
- issue: item.issue.trim(),
167
- stance: item.stance.trim(),
168
- importance,
169
- negotiability,
170
- confidence,
171
- };
172
- })
173
- .filter((item): item is IssuePriority => item !== null)
174
- .slice(0, 10);
175
- }
176
-
177
- function toUnit(value: unknown): number | null {
178
- if (typeof value !== "number" || !Number.isFinite(value)) return null;
179
- return Math.min(1, Math.max(0, value));
180
- }
181
-
182
- // -----------------------------------------------------------------
183
- // Rendering
184
- // -----------------------------------------------------------------
185
-
186
- const BAR_WIDTH = 10;
187
-
188
- export function bar(value: number): string {
189
- const filled = Math.max(0, Math.min(BAR_WIDTH, Math.round(value * BAR_WIDTH)));
190
- return `[${"█".repeat(filled)}${"░".repeat(BAR_WIDTH - filled)}]`;
191
- }
192
-
193
- export function printProfile(profile: TrainingProfile): void {
194
- const lines: string[] = [];
195
- lines.push("");
196
- lines.push(`Summary: ${profile.summary}`);
197
- lines.push("");
198
- lines.push(`Core Values:`);
199
- for (const v of profile.coreValues) lines.push(` - ${v}`);
200
- lines.push("");
201
- lines.push(`Issue Priorities:`);
202
- for (const p of profile.issuePriorities) {
203
- lines.push(
204
- ` ${bar(p.importance)} importance | ${bar(p.negotiability)} negotiability | ${bar(p.confidence)} confidence`,
205
- );
206
- lines.push(` ${p.issue} — ${p.stance}`);
207
- lines.push("");
208
- }
209
- if (profile.redLines.length) {
210
- lines.push(`Red Lines:`);
211
- for (const r of profile.redLines) lines.push(` - ${r}`);
212
- lines.push("");
213
- }
214
- if (profile.acceptableTradeoffs.length) {
215
- lines.push(`Acceptable Tradeoffs:`);
216
- for (const t of profile.acceptableTradeoffs) lines.push(` - ${t}`);
217
- lines.push("");
218
- }
219
- if (profile.uncertaintyAreas.length) {
220
- lines.push(`Uncertainty Areas:`);
221
- for (const u of profile.uncertaintyAreas) lines.push(` - ${u}`);
222
- lines.push("");
223
- }
224
- if (profile.representationRules.length) {
225
- lines.push(`Representation Rules:`);
226
- for (const r of profile.representationRules) lines.push(` - ${r}`);
227
- }
228
- console.log(lines.join("\n"));
229
- }
@@ -1,70 +0,0 @@
1
- /**
2
- * Shared helpers for training LLM prompts.
3
- *
4
- * Mirror of `simocracy-v2/lib/training/prompt-helpers.ts` (subset that
5
- * the CLI actually uses). Keep clamps + wrappers in sync with the web
6
- * app so the same input produces the same prompt locally.
7
- */
8
-
9
- import type { BaselineProposal, BaselineVote, InterviewTurn } from "./types.ts";
10
-
11
- export const CONSTITUTION_MAX_CHARS = 6000;
12
- export const TRANSCRIPT_MAX_TURNS = 16;
13
-
14
- export function clampConstitution(text: string | undefined): string {
15
- if (!text) return "(No current constitution provided)";
16
- const trimmed = text.trim();
17
- if (trimmed.length <= CONSTITUTION_MAX_CHARS) return trimmed;
18
- return `${trimmed.slice(0, CONSTITUTION_MAX_CHARS)}\n\n[…truncated for prompt size; original is ${trimmed.length} chars]`;
19
- }
20
-
21
- export function clampTranscript(turns: InterviewTurn[]): InterviewTurn[] {
22
- if (turns.length <= TRANSCRIPT_MAX_TURNS) return turns;
23
- return turns.slice(-TRANSCRIPT_MAX_TURNS);
24
- }
25
-
26
- export function wrapAsData(label: string, content: string): string {
27
- return `<${label}>\n${content}\n</${label}>`;
28
- }
29
-
30
- export function renderBaselineForPrompt(
31
- baselineVotes: BaselineVote[],
32
- baselineProposals: BaselineProposal[],
33
- ): string {
34
- const enriched = baselineVotes
35
- .map((vote) => {
36
- const proposal = baselineProposals.find((item) => item.id === vote.proposalId);
37
- const reasoning = (vote.reasoning ?? "").trim();
38
- const isUntouched =
39
- vote.vote === "abstain" && Math.abs(vote.importance - 0.5) < 0.01 && !reasoning;
40
- if (isUntouched) return null;
41
- return { vote, proposal, reasoning };
42
- })
43
- .filter(
44
- (entry): entry is { vote: BaselineVote; proposal: BaselineProposal | undefined; reasoning: string } =>
45
- entry !== null,
46
- );
47
-
48
- if (enriched.length === 0) {
49
- return "(The user has not voted on any baseline proposals yet.)";
50
- }
51
-
52
- const ranked = enriched
53
- .map((entry) => {
54
- const cast = entry.vote.vote === "abstain" ? 0 : 1;
55
- return { ...entry, score: cast * 10 + entry.vote.importance };
56
- })
57
- .sort((a, b) => b.score - a.score);
58
-
59
- return ranked
60
- .map(({ vote, proposal, reasoning }, index) => {
61
- const title = proposal?.title ?? vote.proposalId;
62
- const topic = proposal?.topic ? ` (${proposal.topic})` : "";
63
- const verdict = vote.vote.toUpperCase();
64
- const importance = vote.importance.toFixed(2);
65
- const summary = proposal?.summary ? ` — proposal: "${proposal.summary}"` : "";
66
- const comment = reasoning ? `\n Comment from user: "${reasoning}"` : "";
67
- return `${index + 1}. ${verdict} (importance ${importance}) on "${title}"${topic}${summary}${comment}`;
68
- })
69
- .join("\n\n");
70
- }