fix(channel): normalize telegram history for MiniMax
This commit is contained in:
parent
7173045f1c
commit
c5834b1077
2 changed files with 135 additions and 9 deletions
|
|
@ -144,6 +144,11 @@ Optional:
|
||||||
- `MINIMAX_OAUTH_REGION=global` or `cn` (defaults by provider alias)
|
- `MINIMAX_OAUTH_REGION=global` or `cn` (defaults by provider alias)
|
||||||
- `MINIMAX_OAUTH_CLIENT_ID` to override the default OAuth client id
|
- `MINIMAX_OAUTH_CLIENT_ID` to override the default OAuth client id
|
||||||
|
|
||||||
|
Channel compatibility note:
|
||||||
|
|
||||||
|
- For MiniMax-backed channel conversations, runtime history is normalized to keep valid `user`/`assistant` turn order.
|
||||||
|
- Channel-specific delivery guidance (for example Telegram attachment markers) is merged into the leading system prompt instead of being appended as a trailing `system` turn.
|
||||||
|
|
||||||
## Qwen Code OAuth Setup (config.toml)
|
## Qwen Code OAuth Setup (config.toml)
|
||||||
|
|
||||||
Set Qwen Code OAuth mode in config:
|
Set Qwen Code OAuth mode in config:
|
||||||
|
|
|
||||||
|
|
@ -200,6 +200,43 @@ fn channel_delivery_instructions(channel_name: &str) -> Option<&'static str> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn build_channel_system_prompt(base_prompt: &str, channel_name: &str) -> String {
|
||||||
|
if let Some(instructions) = channel_delivery_instructions(channel_name) {
|
||||||
|
if base_prompt.is_empty() {
|
||||||
|
instructions.to_string()
|
||||||
|
} else {
|
||||||
|
format!("{base_prompt}\n\n{instructions}")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
base_prompt.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_cached_channel_turns(turns: Vec<ChatMessage>) -> Vec<ChatMessage> {
|
||||||
|
let mut normalized = Vec::with_capacity(turns.len());
|
||||||
|
let mut expecting_user = true;
|
||||||
|
|
||||||
|
for turn in turns {
|
||||||
|
match (expecting_user, turn.role.as_str()) {
|
||||||
|
(true, "user") => {
|
||||||
|
normalized.push(turn);
|
||||||
|
expecting_user = false;
|
||||||
|
}
|
||||||
|
(false, "assistant") => {
|
||||||
|
normalized.push(turn);
|
||||||
|
expecting_user = true;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if normalized.last().map_or(false, |msg| msg.role == "user") {
|
||||||
|
normalized.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
normalized
|
||||||
|
}
|
||||||
|
|
||||||
fn supports_runtime_model_switch(channel_name: &str) -> bool {
|
fn supports_runtime_model_switch(channel_name: &str) -> bool {
|
||||||
matches!(channel_name, "telegram" | "discord")
|
matches!(channel_name, "telegram" | "discord")
|
||||||
}
|
}
|
||||||
|
|
@ -318,7 +355,7 @@ fn compact_sender_history(ctx: &ChannelRuntimeContext, sender_key: &str) -> bool
|
||||||
let keep_from = turns
|
let keep_from = turns
|
||||||
.len()
|
.len()
|
||||||
.saturating_sub(CHANNEL_HISTORY_COMPACT_KEEP_MESSAGES);
|
.saturating_sub(CHANNEL_HISTORY_COMPACT_KEEP_MESSAGES);
|
||||||
let mut compacted = turns[keep_from..].to_vec();
|
let mut compacted = normalize_cached_channel_turns(turns[keep_from..].to_vec());
|
||||||
|
|
||||||
for turn in &mut compacted {
|
for turn in &mut compacted {
|
||||||
if turn.content.chars().count() > CHANNEL_HISTORY_COMPACT_CONTENT_CHARS {
|
if turn.content.chars().count() > CHANNEL_HISTORY_COMPACT_CONTENT_CHARS {
|
||||||
|
|
@ -327,6 +364,11 @@ fn compact_sender_history(ctx: &ChannelRuntimeContext, sender_key: &str) -> bool
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if compacted.is_empty() {
|
||||||
|
turns.clear();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
*turns = compacted;
|
*turns = compacted;
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
@ -783,21 +825,19 @@ async fn process_channel_message(
|
||||||
ChatMessage::user(&enriched_message),
|
ChatMessage::user(&enriched_message),
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut prior_turns = ctx
|
// Build history from per-sender conversation cache.
|
||||||
|
let prior_turns_raw = ctx
|
||||||
.conversation_histories
|
.conversation_histories
|
||||||
.lock()
|
.lock()
|
||||||
.unwrap_or_else(|e| e.into_inner())
|
.unwrap_or_else(|e| e.into_inner())
|
||||||
.get(&history_key)
|
.get(&history_key)
|
||||||
.cloned()
|
.cloned()
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
let prior_turns = normalize_cached_channel_turns(prior_turns_raw);
|
||||||
|
|
||||||
let mut history = vec![ChatMessage::system(ctx.system_prompt.as_str())];
|
let system_prompt = build_channel_system_prompt(ctx.system_prompt.as_str(), &msg.channel);
|
||||||
history.append(&mut prior_turns);
|
let mut history = vec![ChatMessage::system(system_prompt)];
|
||||||
|
history.extend(prior_turns);
|
||||||
if let Some(instructions) = channel_delivery_instructions(&msg.channel) {
|
|
||||||
history.push(ChatMessage::system(instructions));
|
|
||||||
}
|
|
||||||
|
|
||||||
let use_streaming = target_channel
|
let use_streaming = target_channel
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.is_some_and(|ch| ch.supports_draft_updates());
|
.is_some_and(|ch| ch.supports_draft_updates());
|
||||||
|
|
@ -3994,6 +4034,87 @@ mod tests {
|
||||||
assert!(calls[1][3].1.contains("follow up"));
|
assert!(calls[1][3].1.contains("follow up"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn process_channel_message_telegram_keeps_system_instruction_at_top_only() {
|
||||||
|
let channel_impl = Arc::new(TelegramRecordingChannel::default());
|
||||||
|
let channel: Arc<dyn Channel> = channel_impl.clone();
|
||||||
|
|
||||||
|
let mut channels_by_name = HashMap::new();
|
||||||
|
channels_by_name.insert(channel.name().to_string(), channel);
|
||||||
|
|
||||||
|
let provider_impl = Arc::new(HistoryCaptureProvider::default());
|
||||||
|
let mut histories = HashMap::new();
|
||||||
|
histories.insert(
|
||||||
|
"telegram_alice".to_string(),
|
||||||
|
vec![
|
||||||
|
ChatMessage::assistant("stale assistant"),
|
||||||
|
ChatMessage::user("earlier user question"),
|
||||||
|
ChatMessage::assistant("earlier assistant reply"),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
let runtime_ctx = Arc::new(ChannelRuntimeContext {
|
||||||
|
channels_by_name: Arc::new(channels_by_name),
|
||||||
|
provider: provider_impl.clone(),
|
||||||
|
default_provider: Arc::new("test-provider".to_string()),
|
||||||
|
memory: Arc::new(NoopMemory),
|
||||||
|
tools_registry: Arc::new(vec![]),
|
||||||
|
observer: Arc::new(NoopObserver),
|
||||||
|
system_prompt: Arc::new("test-system-prompt".to_string()),
|
||||||
|
model: Arc::new("test-model".to_string()),
|
||||||
|
temperature: 0.0,
|
||||||
|
auto_save_memory: false,
|
||||||
|
max_tool_iterations: 5,
|
||||||
|
min_relevance_score: 0.0,
|
||||||
|
conversation_histories: Arc::new(Mutex::new(histories)),
|
||||||
|
provider_cache: Arc::new(Mutex::new(HashMap::new())),
|
||||||
|
route_overrides: Arc::new(Mutex::new(HashMap::new())),
|
||||||
|
api_key: None,
|
||||||
|
api_url: None,
|
||||||
|
reliability: Arc::new(crate::config::ReliabilityConfig::default()),
|
||||||
|
provider_runtime_options: providers::ProviderRuntimeOptions::default(),
|
||||||
|
workspace_dir: Arc::new(std::env::temp_dir()),
|
||||||
|
message_timeout_secs: CHANNEL_MESSAGE_TIMEOUT_SECS,
|
||||||
|
interrupt_on_new_message: false,
|
||||||
|
multimodal: crate::config::MultimodalConfig::default(),
|
||||||
|
});
|
||||||
|
|
||||||
|
process_channel_message(
|
||||||
|
runtime_ctx.clone(),
|
||||||
|
traits::ChannelMessage {
|
||||||
|
id: "tg-msg-1".to_string(),
|
||||||
|
sender: "alice".to_string(),
|
||||||
|
reply_target: "chat-telegram".to_string(),
|
||||||
|
content: "hello".to_string(),
|
||||||
|
channel: "telegram".to_string(),
|
||||||
|
timestamp: 1,
|
||||||
|
thread_ts: None,
|
||||||
|
},
|
||||||
|
CancellationToken::new(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let calls = provider_impl
|
||||||
|
.calls
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(|e| e.into_inner());
|
||||||
|
assert_eq!(calls.len(), 1);
|
||||||
|
assert_eq!(calls[0].len(), 4);
|
||||||
|
|
||||||
|
let roles = calls[0]
|
||||||
|
.iter()
|
||||||
|
.map(|(role, _)| role.as_str())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
assert_eq!(roles, vec!["system", "user", "assistant", "user"]);
|
||||||
|
assert!(
|
||||||
|
calls[0][0]
|
||||||
|
.1
|
||||||
|
.contains("When responding on Telegram, include media markers"),
|
||||||
|
"telegram delivery instruction should live in the system prompt"
|
||||||
|
);
|
||||||
|
assert!(!calls[0].iter().skip(1).any(|(role, _)| role == "system"));
|
||||||
|
}
|
||||||
|
|
||||||
// ── AIEOS Identity Tests (Issue #168) ─────────────────────────
|
// ── AIEOS Identity Tests (Issue #168) ─────────────────────────
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue