-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathindex.ts
More file actions
196 lines (177 loc) · 6.07 KB
/
Copy pathindex.ts
File metadata and controls
196 lines (177 loc) · 6.07 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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
import { pathToFileURL } from "node:url";
import * as readline from "node:readline/promises";
import {
accessTokenFromEnv,
query,
type CanUseTool,
} from "@qoder-ai/qoder-agent-sdk";
type QuestionOption = {
label: string;
description?: string;
};
type Question = {
question: string;
header: string;
options: QuestionOption[];
multiSelect: boolean;
};
const DEFAULT_PROMPT = `Use AskUserQuestion exactly once before making a recommendation. Ask these two questions:
1. Header "Environment": "Which deployment environment should we use?" with options "Staging" and "Production"; single-select.
2. Header "Checks": "Which validation checks should run?" with options "Unit tests", "Integration tests", and "Security scan"; multi-select.
After receiving the answers, summarize the choices in one sentence. Do not use other tools.`;
const terminal = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
let answeredQuestions = 0;
function parseQuestions(input: Record<string, unknown>): Question[] {
if (!Array.isArray(input.questions) || input.questions.length === 0) {
throw new Error("AskUserQuestion input must contain a non-empty questions array.");
}
return input.questions.map((rawQuestion, questionIndex) => {
if (!rawQuestion || typeof rawQuestion !== "object") {
throw new Error(`Question ${questionIndex + 1} must be an object.`);
}
const value = rawQuestion as Record<string, unknown>;
if (typeof value.question !== "string" || !value.question.trim()) {
throw new Error(`Question ${questionIndex + 1} has no question text.`);
}
if (!Array.isArray(value.options) || value.options.length < 2) {
throw new Error(`Question ${questionIndex + 1} needs at least two options.`);
}
const options = value.options.map((rawOption, optionIndex) => {
if (!rawOption || typeof rawOption !== "object") {
throw new Error(
`Option ${optionIndex + 1} in question ${questionIndex + 1} must be an object.`,
);
}
const option = rawOption as Record<string, unknown>;
if (typeof option.label !== "string" || !option.label.trim()) {
throw new Error(
`Option ${optionIndex + 1} in question ${questionIndex + 1} has no label.`,
);
}
return {
label: option.label,
description:
typeof option.description === "string"
? option.description
: undefined,
};
});
return {
question: value.question,
header:
typeof value.header === "string" && value.header.trim()
? value.header
: `Question ${questionIndex + 1}`,
options,
multiSelect: value.multiSelect === true,
};
});
}
async function readAnswer(question: Question): Promise<string> {
console.log(`\n[${question.header}] ${question.question}`);
question.options.forEach((option, index) => {
const description = option.description ? ` — ${option.description}` : "";
console.log(` ${index + 1}. ${option.label}${description}`);
});
console.log(
question.multiSelect
? " Enter comma-separated numbers, or type a custom answer."
: " Enter one number, or type a custom answer.",
);
while (true) {
const raw = (await terminal.question(" > ")).trim();
if (!raw) return question.options[0].label;
const parts = raw.split(",").map((part) => part.trim());
const numeric = parts.every((part) => /^\d+$/.test(part));
if (numeric) {
const indices = parts.map((part) => Number(part) - 1);
const valid = indices.every(
(index) => index >= 0 && index < question.options.length,
);
if (!valid || (!question.multiSelect && indices.length !== 1)) {
console.log(" Invalid selection; try again.");
continue;
}
return indices.map((index) => question.options[index].label).join(", ");
}
const matches = question.options.filter((option) =>
option.label.toLowerCase().startsWith(raw.toLowerCase()),
);
return matches.length === 1 ? matches[0].label : raw;
}
}
export const respondToAskUserQuestion: CanUseTool = async (
toolName,
input,
) => {
if (toolName !== "AskUserQuestion" && toolName !== "ask_user") {
return {
behavior: "deny",
message: `This sample handles only AskUserQuestion, not ${toolName}.`,
};
}
try {
const questions = parseQuestions(input);
const answers: Record<string, string> = {};
for (const question of questions) {
answers[question.question] = await readAnswer(question);
answeredQuestions += 1;
}
return {
behavior: "allow",
updatedInput: { ...input, answers },
};
} catch (error) {
return {
behavior: "deny",
message: error instanceof Error ? error.message : String(error),
};
}
};
export async function run(prompt: string): Promise<void> {
answeredQuestions = 0;
const stream = query({
prompt,
options: {
auth: accessTokenFromEnv(),
model: "auto",
tools: ["AskUserQuestion"],
permissionMode: "default",
canUseTool: respondToAskUserQuestion,
maxTurns: 3,
},
});
let completed = false;
try {
for await (const message of stream) {
if (message.type === "result") {
if (message.subtype !== "success") {
throw new Error(message.errors?.join("\n") || message.subtype);
}
console.log(`\nassistant> ${message.result}`);
completed = true;
}
}
} finally {
await stream.close();
terminal.close();
}
if (!completed) throw new Error("The query ended without a success result.");
if (answeredQuestions === 0) {
throw new Error("The agent completed without calling AskUserQuestion.");
}
console.log(`\nAnswered ${answeredQuestions} question(s).`);
}
async function main(): Promise<void> {
const prompt = process.argv.slice(2).join(" ") || DEFAULT_PROMPT;
await run(prompt);
}
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
main().catch((error: unknown) => {
console.error(error instanceof Error ? error.message : error);
process.exitCode = 1;
});
}