-
Notifications
You must be signed in to change notification settings - Fork 17
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: external events #19
Comments
Hello @KnorpelSenf , |
I am going to document this more thoroughly in the coming days and weeks. Here is a relevant test case: conversations/test/conversation.test.ts Lines 36 to 59 in 6a543d0
The naming in the above description is not used in the implementation. You can enter a conversation which gives you some state, and then you can pass this state when resuming a conversation. You can view an HTML overview of the plugin's API surface at https://doc.deno.land/https://raw.githubusercontent.com/grammyjs/conversations/refs/heads/engine/src/mod.ts. It says Deno everywhere because they provide the tooling to create such a page, but the plugin works identically on Node.js. Instead of const event = await conversation.wait()
const data = event.update as MyEventData LMK if you have further questions, and apologies for the rough DX. After all, it's not released yet. |
@KnorpelSenf Thank you for your quick and detailed response. However, I'm still a bit confused about the usage of the new API. I'm new to grammY, so please forgive me if my understanding of the technical details is incorrect:
const composer = new Composer<MyContext>();
async function generateImage(conversation: MyConversation, ctx: MyContext) {
conversation.run(hydrate());
const modelKeyboard = new InlineKeyboard()
.text(ctx.t("model-sd"), "sd")
.text(ctx.t("model-flux"), "flux");
await ctx.reply(
ctx.t("pls-select-model"),
{ reply_markup: modelKeyboard }
);
const modelCtx = await conversation.waitForCallbackQuery(["sd", "flux"] , {
otherwise: (ctx) => ctx.reply(ctx.t("select-model"), { reply_markup: modelKeyboard}),
});
await modelCtx.answerCallbackQuery();
const model = modelCtx.match;
// prompt
// similar code to get prompt
const prompt = ...;
let task: ImageTask = await conversation.external(async () => await prisma.imageTask.create({
data: {
model: model.toString(),
prompt: prompt.toString(),
},
}));
console.log("task created: " + task.id);
await ctx.reply(ctx.t("task-created"));
let secs = 0;
while (task.status !== 'SUCCESS') {
const statusMessage = await ctx.reply("waiting " + secs + "s");
await conversation.sleep(1000);
task = await conversation.external(async () => await prisma.imageTask.findUniqueOrThrow({where: { id: task.id }}));
}I
await ctx.reply("success");
}
composer.use(createConversation(generateImage));
composer.command("generate-image", (ctx) => ctx.conversation.enter("generateImage")); However, this turned out to be an incorrect usage, as the program reported a webhook timeout error. How can this code be rewritten using the new features? |
@KnorpelSenf Hello again, after reviewing the source code, I've noticed that both
|
Got a bit much to do right now, let me get back to you this weekend, sorry for the delay |
Maybe as a very short (and probably too short) hint, you'll have to provide a storage adapter and then query that storage yourself in order to obtain conversation state. This state can in turn be passed to |
@KnorpelSenf There's no rush at all, please take your time with your other tasks. If possible, a concrete example would be very helpful when you have a chance to provide one, thanks. |
I have pushed a few commits that make this case easier. You'll need to pull a new version. This is how you can manually load state and use type MyContext = ConversationFlavor<Context>;
type MyConversationContext = Context;
type MyConversation = Conversation<MyConversationContext>;
const bot = new Bot<MyContext>("redacted");
const version = 0;
const fileAdapter = new FileAdapter<VersionedState<ConversationData>>({
dirName: "/tmp",
});
bot.use(
conversations({
storage: {
type: "key",
version,
getStorageKey: (ctx) => ctx.chatId?.toString(),
adapter: fileAdapter,
},
}),
);
bot.command(
"active",
(ctx) => ctx.reply(JSON.stringify(ctx.conversation.active(), null, 2)),
);
interface MyEvent {
type: "event";
foo: string;
bar: number;
}
async function convo(conversation: MyConversation, ctx: MyConversationContext) {
const { update } = await conversation.waitUntil((ctx) =>
// only wait for external events
"type" in ctx.update && ctx.update.type === "event"
);
const event = update as unknown as MyEvent; // cast back from update until plugin is improved
await ctx.reply(`Received ${event.foo}`);
}
bot.use(createConversation(convo));
bot.command("start", (ctx) => ctx.conversation.enter("convo"));
const { versionify, unpack } = pinVersion(version);
async function supplyExternalEvent(chat_id: number, event: MyEvent) {
// fetch data
const key = chat_id.toString();
const data = await fileAdapter.read(key);
const state = unpack(data);
if (state === undefined) return; // bad or missing data for chat_id
const convoState = state.convo?.[0];
if (convoState === undefined) return; // convo not entered
const baseData = {
update: event as unknown as Update, // cast to update until plugin is improved
api: bot.api,
me: bot.botInfo,
};
// run conversation
const res = await resumeConversation(convo, baseData, convoState);
// handle result
switch (res.status) {
case "skipped":
return;
case "complete":
case "error":
await fileAdapter.delete(key);
return;
case "handled": {
const newState: ConversationState = {
args: convoState.args,
interrupts: res.interrupts,
replay: res.replay,
};
state.convo[0] = newState;
await fileAdapter.write(key, versionify(state));
return;
}
}
}
bot.start(); Here are a few interesting things to note about the above code:
|
@KnorpelSenf Thank you very much, your code is very clear, detailed, and helpful! |
It may be interesting to be able to wait for external events, rather than just waiting for Telegram updates.
For example, we could add something like
await conversation.waitExternal('id')
and then in turn people can dowhich loads the right session data, and runs the supplied conversation function.
The text was updated successfully, but these errors were encountered: