Skip to content

Commit f7c138d

Browse files
nolikclaude
andauthored
refactor(chat_gpt): decompose the two large handlers (#607)
Splits the ~80-line handle_chat_gpt_question and ~60-line handle_reply into small, single-purpose helpers (§4): - `bot_configuration_for_message` / `bot_configuration_for_profile`: pure profile-selection functions, now covered by unit tests. - `build_question_context`: the summary-vs-rolling context routing. - `send_gpt_reply`: thread-aware reply send (used by the question path). handle_reply is additionally flattened from `if let Ok { .. }` to a `let Ok(..) else { return Ok(()) }` guard. Its send stays non-threaded to preserve exact current behavior. Pure refactor — no happy-path behavior change (chat_gpt_e2e is the backstop). 3 new unit tests; local gate green (fmt, clippy -D warnings, 11 unit tests, integration tests compile). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 4e0785e commit f7c138d

1 file changed

Lines changed: 158 additions & 80 deletions

File tree

src/chat_gpt_handler.rs

Lines changed: 158 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use redis::aio::ConnectionManager;
1010
use regex::Regex;
1111
use serde::{Deserialize, Serialize};
1212
use teloxide::prelude::*;
13-
use teloxide::types::ReplyParameters;
13+
use teloxide::types::{MessageId, ReplyParameters, ThreadId};
1414
use teloxide::RequestError;
1515

1616
const FEDOR_CHAT_GPT_SYSTEM_CONTEXT: &str = "Предоставь грубый ответ. \
@@ -76,55 +76,37 @@ pub async fn handle_chat_gpt_question(
7676
return Ok(());
7777
};
7878
info!("gpt invocation: chat_id: {chat_id}, message: {message}");
79-
let bot_profiles = &*BOT_PROFILES;
80-
let bot_configuration = bot_profiles
81-
.iter()
82-
.find(|&x| x.is_correct_config(message))
83-
.unwrap_or(&bot_profiles[0]);
84-
let bot_context_key = &format!("{:#?}:chat:{:#?}", bot_configuration.profile, chat_id.0);
79+
80+
let bot_configuration = bot_configuration_for_message(message);
81+
let bot_context_key = format!("{:#?}:chat:{:#?}", bot_configuration.profile, chat_id.0);
8582
let user_message = ChatMessage {
8683
role: User,
8784
content: message.to_string(),
8885
};
89-
let summary_request_regex = &*CHAT_SUMMARY_REQUEST_REGEX;
9086
let mut redis_cm = gpt_parameters.redis_connection_manager.clone();
91-
let context = match summary_request_regex.is_match(message) {
92-
true => {
93-
fetch_chat_summary_context(
94-
&mut redis_cm,
95-
chat_id.0,
96-
&user_message,
97-
bot_configuration.gpt_system_context,
98-
)
99-
.await
100-
}
101-
false => {
102-
fetch_bot_context(
103-
&mut redis_cm,
104-
bot_context_key,
105-
&user_message,
106-
bot_configuration.gpt_system_context,
107-
)
108-
.await
109-
}
110-
};
87+
let context = build_question_context(
88+
&mut redis_cm,
89+
chat_id,
90+
&bot_context_key,
91+
&user_message,
92+
bot_configuration,
93+
)
94+
.await;
11195

11296
let gpt_response_message = gpt_service::chat_gpt_call(gpt_parameters, chat_id, context).await;
113-
let bot_reply_msg_response = if let Some(thread_id) = msg.thread_id {
114-
bot.send_message(chat_id, &gpt_response_message.content)
115-
.reply_parameters(ReplyParameters::new(msg.id))
116-
.message_thread_id(thread_id)
117-
.await
118-
} else {
119-
bot.send_message(chat_id, &gpt_response_message.content)
120-
.reply_parameters(ReplyParameters::new(msg.id))
121-
.await
122-
};
97+
let bot_reply_msg_response = send_gpt_reply(
98+
&bot,
99+
chat_id,
100+
msg.id,
101+
msg.thread_id,
102+
&gpt_response_message.content,
103+
)
104+
.await;
123105

124106
update_bot_context_and_identifiers(
125107
&mut redis_cm,
126108
bot_configuration.profile,
127-
bot_context_key,
109+
&bot_context_key,
128110
&user_message,
129111
&gpt_response_message,
130112
bot_reply_msg_response,
@@ -133,6 +115,72 @@ pub async fn handle_chat_gpt_question(
133115
Ok(())
134116
}
135117

118+
/// Pick the bot profile whose mention regex matches the message, falling back
119+
/// to the first profile when none match.
120+
fn bot_configuration_for_message(message: &str) -> &'static BotConfiguration<'static> {
121+
let bot_profiles = &*BOT_PROFILES;
122+
bot_profiles
123+
.iter()
124+
.find(|&config| config.is_correct_config(message))
125+
.unwrap_or(&bot_profiles[0])
126+
}
127+
128+
/// Look up the bot profile a previous bot message was sent under, falling back
129+
/// to the first profile when it is unknown.
130+
fn bot_configuration_for_profile(profile: BotProfile) -> &'static BotConfiguration<'static> {
131+
let bot_profiles = &*BOT_PROFILES;
132+
bot_profiles
133+
.iter()
134+
.find(|&config| config.profile == profile)
135+
.unwrap_or(&bot_profiles[0])
136+
}
137+
138+
/// Build the GPT context for a fresh question: a chat-history summary when the
139+
/// message asks "what's going on", otherwise the profile's rolling context.
140+
async fn build_question_context(
141+
redis_cm: &mut ConnectionManager,
142+
chat_id: ChatId,
143+
bot_context_key: &String,
144+
user_message: &ChatMessage,
145+
bot_configuration: &BotConfiguration<'_>,
146+
) -> Vec<ChatMessage> {
147+
if CHAT_SUMMARY_REQUEST_REGEX.is_match(&user_message.content) {
148+
fetch_chat_summary_context(
149+
redis_cm,
150+
chat_id.0,
151+
user_message,
152+
bot_configuration.gpt_system_context,
153+
)
154+
.await
155+
} else {
156+
fetch_bot_context(
157+
redis_cm,
158+
bot_context_key,
159+
user_message,
160+
bot_configuration.gpt_system_context,
161+
)
162+
.await
163+
}
164+
}
165+
166+
/// Send a bot reply, routing it into the originating message thread when there
167+
/// is one.
168+
async fn send_gpt_reply(
169+
bot: &Bot,
170+
chat_id: ChatId,
171+
reply_to: MessageId,
172+
thread_id: Option<ThreadId>,
173+
content: &str,
174+
) -> Result<Message, RequestError> {
175+
let request = bot
176+
.send_message(chat_id, content.to_owned())
177+
.reply_parameters(ReplyParameters::new(reply_to));
178+
match thread_id {
179+
Some(thread_id) => request.message_thread_id(thread_id).await,
180+
None => request.await,
181+
}
182+
}
183+
136184
async fn update_bot_context_and_identifiers(
137185
redis_connection_manager: &mut ConnectionManager,
138186
bot_profile: BotProfile,
@@ -182,51 +230,48 @@ pub async fn handle_reply(
182230
info!("chat_key: {chat_key:?}");
183231
let reply_msg_id = reply_msg.id.0;
184232
let mut redis_cm = gpt_parameters.redis_connection_manager.clone();
185-
if let Ok(reply_msg_bot_profile) =
233+
let Ok(reply_msg_bot_profile) =
186234
chat_repository::get_bot_msg_profile(&mut redis_cm, chat_key, reply_msg_id).await
187-
{
188-
info!(
189-
"handle msg of bot msg reply_msg_id:'{reply_msg_id:#?}' under bot profile:'{reply_msg_bot_profile:#?}'"
190-
);
191-
info!(
192-
"truing to reply chat_id:{chat_id:#?}, msg_id: {:?}, thread_id: {:#?}",
193-
msg.id, msg.thread_id
194-
);
195-
let bot_profiles = &*BOT_PROFILES;
196-
let bot_configuration = bot_profiles
197-
.iter()
198-
.find(|&x| x.profile == reply_msg_bot_profile)
199-
.unwrap_or(&bot_profiles[0]);
200-
let bot_context_key = &format!("{:#?}:chat:{:#?}", bot_configuration.profile, chat_id.0);
201-
let user_message = ChatMessage {
202-
role: User,
203-
content: message.to_string(),
204-
};
205-
let context = fetch_bot_context(
206-
&mut redis_cm,
207-
bot_context_key,
208-
&user_message,
209-
bot_configuration.gpt_system_context,
210-
)
211-
.await;
235+
else {
236+
return Ok(());
237+
};
238+
info!(
239+
"handle msg of bot msg reply_msg_id:'{reply_msg_id:#?}' under bot profile:'{reply_msg_bot_profile:#?}'"
240+
);
241+
info!(
242+
"truing to reply chat_id:{chat_id:#?}, msg_id: {:?}, thread_id: {:#?}",
243+
msg.id, msg.thread_id
244+
);
212245

213-
let gpt_response_message =
214-
gpt_service::chat_gpt_call(gpt_parameters, chat_id, context).await;
215-
let bot_reply_msg_response = bot
216-
.send_message(chat_id, &gpt_response_message.content)
217-
.reply_parameters(ReplyParameters::new(msg.id))
218-
.await;
246+
let bot_configuration = bot_configuration_for_profile(reply_msg_bot_profile);
247+
let bot_context_key = format!("{:#?}:chat:{:#?}", bot_configuration.profile, chat_id.0);
248+
let user_message = ChatMessage {
249+
role: User,
250+
content: message.to_string(),
251+
};
252+
let context = fetch_bot_context(
253+
&mut redis_cm,
254+
&bot_context_key,
255+
&user_message,
256+
bot_configuration.gpt_system_context,
257+
)
258+
.await;
219259

220-
update_bot_context_and_identifiers(
221-
&mut redis_cm,
222-
bot_configuration.profile,
223-
bot_context_key,
224-
&user_message,
225-
&gpt_response_message,
226-
bot_reply_msg_response,
227-
)
260+
let gpt_response_message = gpt_service::chat_gpt_call(gpt_parameters, chat_id, context).await;
261+
let bot_reply_msg_response = bot
262+
.send_message(chat_id, &gpt_response_message.content)
263+
.reply_parameters(ReplyParameters::new(msg.id))
228264
.await;
229-
}
265+
266+
update_bot_context_and_identifiers(
267+
&mut redis_cm,
268+
bot_configuration.profile,
269+
&bot_context_key,
270+
&user_message,
271+
&gpt_response_message,
272+
bot_reply_msg_response,
273+
)
274+
.await;
230275
Ok(())
231276
}
232277

@@ -300,6 +345,7 @@ pub enum BotProfile {
300345

301346
#[cfg(test)]
302347
mod tests {
348+
use super::{bot_configuration_for_message, bot_configuration_for_profile, BotProfile};
303349
use crate::chat_gpt_handler::SUMMARY_REQUEST_REGEX;
304350
use regex::Regex;
305351

@@ -311,4 +357,36 @@ mod tests {
311357
assert!(summary_regex.is_match("Фёдор, что тут происходит?"));
312358
assert!(!summary_regex.is_match("Fedor, kak dela?"));
313359
}
360+
361+
#[test]
362+
fn bot_configuration_for_message_selects_by_mention() {
363+
assert_eq!(
364+
bot_configuration_for_message("привет fedor").profile,
365+
BotProfile::Fedor
366+
);
367+
assert_eq!(
368+
bot_configuration_for_message("а вот и феликс").profile,
369+
BotProfile::Felix
370+
);
371+
assert_eq!(
372+
bot_configuration_for_message("ferris the crab").profile,
373+
BotProfile::Ferris
374+
);
375+
}
376+
377+
#[test]
378+
fn bot_configuration_for_message_defaults_to_first_profile() {
379+
// No mention keyword -> first configured profile (Fedor).
380+
assert_eq!(
381+
bot_configuration_for_message("nothing relevant here").profile,
382+
BotProfile::Fedor
383+
);
384+
}
385+
386+
#[test]
387+
fn bot_configuration_for_profile_round_trips() {
388+
for profile in [BotProfile::Fedor, BotProfile::Felix, BotProfile::Ferris] {
389+
assert_eq!(bot_configuration_for_profile(profile).profile, profile);
390+
}
391+
}
314392
}

0 commit comments

Comments
 (0)