forked from earendil-works/pi
-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathqna.ts
More file actions
122 lines (102 loc) · 3.65 KB
/
Copy pathqna.ts
File metadata and controls
122 lines (102 loc) · 3.65 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
/**
* Q&A extraction extension - extracts questions from assistant responses
*
* Demonstrates the "prompt generator" pattern:
* 1. /qna command gets the last assistant message
* 2. Shows a spinner while extracting (hides editor)
* 3. Loads the result into the editor for user to fill in answers
*/
import { complete, type UserMessage } from "@earendil-works/pi-ai";
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { BorderedLoader } from "@earendil-works/pi-coding-agent";
const SYSTEM_PROMPT = `You are a question extractor. Given text from a conversation, extract any questions that need answering and format them for the user to fill in.
Output format:
- List each question on its own line, prefixed with "Q: "
- After each question, add a blank line for the answer prefixed with "A: "
- If no questions are found, output "No questions found in the last message."
Example output:
Q: What is your preferred database?
A:
Q: Should we use TypeScript or JavaScript?
A:
Keep questions in the order they appeared. Be concise.`;
export default function (pi: ExtensionAPI) {
pi.registerCommand("qna", {
description: "Extract questions from last assistant message into editor",
handler: async (_args, ctx) => {
if (!ctx.hasUI) {
ctx.ui.notify("qna requires interactive mode", "error");
return;
}
if (!ctx.model) {
ctx.ui.notify("No model selected", "error");
return;
}
// Find the last assistant message on the current branch
const branch = ctx.sessionManager.getBranch();
let lastAssistantText: string | undefined;
for (let i = branch.length - 1; i >= 0; i--) {
const entry = branch[i];
if (entry.type === "message") {
const msg = entry.message;
if ("role" in msg && msg.role === "assistant") {
if (msg.stopReason !== "stop") {
ctx.ui.notify(`Last assistant message incomplete (${msg.stopReason})`, "error");
return;
}
const textParts = msg.content
.filter((c): c is { type: "text"; text: string } => c.type === "text")
.map((c) => c.text);
if (textParts.length > 0) {
lastAssistantText = textParts.join("\n");
break;
}
}
}
}
if (!lastAssistantText) {
ctx.ui.notify("No assistant messages found", "error");
return;
}
// Run extraction with loader UI
const result = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
const loader = new BorderedLoader(tui, theme, `Extracting questions using ${ctx.model!.id}...`);
loader.onAbort = () => done(null);
// Do the work
const doExtract = async () => {
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(ctx.model!);
if (!auth.ok || !auth.apiKey) {
throw new Error(auth.ok ? `No API key for ${ctx.model!.provider}` : auth.error);
}
const userMessage: UserMessage = {
role: "user",
content: [{ type: "text", text: lastAssistantText! }],
timestamp: Date.now(),
};
const response = await complete(
ctx.model!,
{ systemPrompt: SYSTEM_PROMPT, messages: [userMessage] },
{ apiKey: auth.apiKey, headers: auth.headers, signal: loader.signal },
);
if (response.stopReason === "aborted") {
return null;
}
return response.content
.filter((c): c is { type: "text"; text: string } => c.type === "text")
.map((c) => c.text)
.join("\n");
};
doExtract()
.then(done)
.catch(() => done(null));
return loader;
});
if (result === null) {
ctx.ui.notify("Cancelled", "info");
return;
}
ctx.ui.setEditorText(result);
ctx.ui.notify("Questions loaded. Edit and submit when ready.", "info");
},
});
}