Compare commits
4 Commits
8f82009c32
...
2d5b6168cc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d5b6168cc | ||
|
|
cd97ac0f57 | ||
|
|
3c2650824c | ||
|
|
4ec4e2b993 |
@ -11,31 +11,19 @@ use crate::agent::{AgentError, AgentRuntimeConfig};
|
||||
const TOKEN_ESTIMATE_CHARS_PER_TOKEN: usize = 4;
|
||||
const TOKEN_ESTIMATE_SAFETY_MULTIPLIER: f64 = 1.2;
|
||||
|
||||
/// Token estimation using ~4 chars/token heuristic with 1.2x safety margin.
|
||||
/// Token estimation using JSON serialization (matches actual request size)
|
||||
pub fn estimate_tokens(messages: &[ChatMessage]) -> usize {
|
||||
let raw: usize = messages
|
||||
.iter()
|
||||
.map(|message| {
|
||||
message
|
||||
.content
|
||||
.len()
|
||||
.div_ceil(TOKEN_ESTIMATE_CHARS_PER_TOKEN)
|
||||
+ estimate_image_tokens(&message.media_refs)
|
||||
+ 4
|
||||
})
|
||||
.sum();
|
||||
(raw as f64 * TOKEN_ESTIMATE_SAFETY_MULTIPLIER) as usize
|
||||
}
|
||||
// Serialize to JSON to match actual request format sent to LLM
|
||||
let serialized_len = serde_json::to_string(messages)
|
||||
.map(|s| s.len())
|
||||
.unwrap_or_else(|_| {
|
||||
// Fallback: use content length if serialization fails
|
||||
messages.iter().map(|m| m.content.len()).sum()
|
||||
});
|
||||
|
||||
fn estimate_image_tokens(media_refs: &[String]) -> usize {
|
||||
media_refs
|
||||
.iter()
|
||||
.filter_map(|path| std::fs::metadata(path).ok())
|
||||
.map(|metadata| {
|
||||
let base64_chars = metadata.len().saturating_mul(4).div_ceil(3) as usize;
|
||||
base64_chars.div_ceil(TOKEN_ESTIMATE_CHARS_PER_TOKEN)
|
||||
})
|
||||
.sum()
|
||||
// Apply safety margin for token estimation
|
||||
((serialized_len / TOKEN_ESTIMATE_CHARS_PER_TOKEN) as f64
|
||||
* TOKEN_ESTIMATE_SAFETY_MULTIPLIER) as usize
|
||||
}
|
||||
|
||||
/// Configuration for context compression.
|
||||
@ -502,13 +490,13 @@ mod tests {
|
||||
];
|
||||
|
||||
let tokens = estimate_tokens(&messages);
|
||||
// "Hello" (5) -> ceil(5/4)+4 = 2+4 = 6
|
||||
// "Hi there!" (8) -> ceil(8/4)+4 = 2+4 = 6
|
||||
// "How are you?" (11) -> ceil(11/4)+4 = 3+4 = 7
|
||||
// raw = 19, with 1.2x = ~23
|
||||
// JSON serialization includes: id, role, content, timestamp, etc.
|
||||
// With 3 messages, the JSON overhead is significant
|
||||
// Serialized JSON is typically 300-500 chars for 3 simple messages
|
||||
// 500 / 4 * 1.2 = ~150 tokens
|
||||
assert!(
|
||||
tokens > 18 && tokens < 30,
|
||||
"Expected ~23 tokens, got {}",
|
||||
tokens > 50 && tokens < 300,
|
||||
"Expected ~100-200 tokens (JSON overhead), got {}",
|
||||
tokens
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
use crate::agent::context_compressor::estimate_tokens;
|
||||
use crate::command::context::CommandContext;
|
||||
use crate::command::handler::{CommandHandler, CommandMetadata};
|
||||
use crate::command::handlers::get_messages_from_session;
|
||||
use crate::command::response::{CommandError, CommandResponse, MessageKind};
|
||||
use crate::command::Command;
|
||||
use crate::gateway::session::SessionManager;
|
||||
use crate::storage::SessionStore;
|
||||
use async_trait::async_trait;
|
||||
use std::sync::Arc;
|
||||
@ -9,11 +12,20 @@ use std::sync::Arc;
|
||||
/// 获取当前话题命令处理器
|
||||
pub struct GetCurrentSessionCommandHandler {
|
||||
store: Arc<SessionStore>,
|
||||
session_manager: Option<SessionManager>,
|
||||
}
|
||||
|
||||
impl GetCurrentSessionCommandHandler {
|
||||
pub fn new(store: Arc<SessionStore>) -> Self {
|
||||
Self { store }
|
||||
Self {
|
||||
store,
|
||||
session_manager: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_session_manager(mut self, session_manager: SessionManager) -> Self {
|
||||
self.session_manager = Some(session_manager);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,20 +62,34 @@ async fn handle_get_current_session(
|
||||
let topic_id = ctx.topic_id.as_deref()
|
||||
.ok_or_else(|| CommandError::new("NO_CURRENT_TOPIC", "No current topic"))?;
|
||||
|
||||
let chat_id = ctx.chat_id.as_deref()
|
||||
.ok_or_else(|| CommandError::new("NO_CHAT_ID", "No chat id".to_string()))?;
|
||||
|
||||
let topic = handler
|
||||
.store
|
||||
.get_topic(topic_id)
|
||||
.map_err(|e| CommandError::new("GET_TOPIC_ERROR", e.to_string()))?
|
||||
.ok_or_else(|| CommandError::new("TOPIC_NOT_FOUND", format!("Topic not found: {}", topic_id)))?;
|
||||
|
||||
// Load messages from session memory
|
||||
let messages = get_messages_from_session(
|
||||
&handler.session_manager,
|
||||
&ctx.channel_name,
|
||||
chat_id,
|
||||
).await?;
|
||||
|
||||
let actual_message_count = messages.len();
|
||||
let estimated_tokens = estimate_tokens(&messages);
|
||||
|
||||
let last_active = format_time_ago(topic.last_active_at);
|
||||
let created_at = format_time_ago(topic.created_at);
|
||||
|
||||
let message = format!(
|
||||
"Current Topic:\n\n Topic ID: {}\n Title: {}\n Messages: {}\n Created: {}\n Last Active: {}",
|
||||
"Current Topic:\n\n Topic ID: {}\n Title: {}\n Messages: {}\n Tokens: ~{}\n Created: {}\n Last Active: {}",
|
||||
topic.id,
|
||||
topic.title,
|
||||
topic.message_count,
|
||||
actual_message_count,
|
||||
estimated_tokens,
|
||||
created_at,
|
||||
last_active
|
||||
);
|
||||
@ -72,7 +98,8 @@ async fn handle_get_current_session(
|
||||
.with_message(MessageKind::Notification, &message)
|
||||
.with_metadata("topic_id", &topic.id)
|
||||
.with_metadata("title", &topic.title)
|
||||
.with_metadata("message_count", &topic.message_count.to_string()))
|
||||
.with_metadata("message_count", &actual_message_count.to_string())
|
||||
.with_metadata("estimated_tokens", &estimated_tokens.to_string()))
|
||||
}
|
||||
|
||||
fn format_time_ago(timestamp_ms: i64) -> String {
|
||||
|
||||
@ -12,3 +12,35 @@ pub use save_session::{
|
||||
escape_yaml_string, format_message_content, format_timestamp,
|
||||
generate_messages_markdown, generate_system_prompt_markdown,
|
||||
};
|
||||
|
||||
use crate::bus::ChatMessage;
|
||||
use crate::command::response::CommandError;
|
||||
use crate::gateway::session::SessionManager;
|
||||
|
||||
/// 从 Session 内存获取消息历史(供命令使用)
|
||||
pub async fn get_messages_from_session(
|
||||
session_manager: &Option<SessionManager>,
|
||||
channel_name: &str,
|
||||
chat_id: &str,
|
||||
) -> Result<Vec<ChatMessage>, CommandError> {
|
||||
let session_manager = session_manager.as_ref().ok_or_else(|| {
|
||||
CommandError::new(
|
||||
"SESSION_MANAGER_NOT_SET",
|
||||
"Session manager not configured".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
match session_manager.get(channel_name).await {
|
||||
Some(session) => {
|
||||
let guard = session.lock().await;
|
||||
Ok(guard
|
||||
.get_history(chat_id)
|
||||
.map(|m| m.clone())
|
||||
.unwrap_or_default())
|
||||
}
|
||||
None => Err(CommandError::new(
|
||||
"SESSION_NOT_FOUND",
|
||||
format!("Session not found for channel: {}", channel_name),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,12 +1,14 @@
|
||||
use crate::agent::{SystemPrompt, SystemPromptContext, SystemPromptProvider};
|
||||
use crate::bus::ChatMessage;
|
||||
use crate::command::context::CommandContext;
|
||||
use crate::command::handler::{CommandHandler, CommandMetadata};
|
||||
use crate::command::handlers::{
|
||||
escape_yaml_string, format_timestamp, generate_messages_markdown,
|
||||
generate_system_prompt_markdown,
|
||||
generate_system_prompt_markdown, get_messages_from_session,
|
||||
};
|
||||
use crate::command::response::{CommandError, CommandResponse, MessageKind};
|
||||
use crate::command::Command;
|
||||
use crate::gateway::session::SessionManager;
|
||||
use crate::storage::{SessionStore, TopicRecord};
|
||||
use async_trait::async_trait;
|
||||
use chrono::Local;
|
||||
@ -19,6 +21,7 @@ pub async fn save_topic_to_file(
|
||||
filepath: Option<String>,
|
||||
store: &SessionStore,
|
||||
system_prompt_provider: &dyn SystemPromptProvider,
|
||||
messages: &[ChatMessage], // ← 从外部传入的消息(已压缩的 active history)
|
||||
) -> Result<PathBuf, String> {
|
||||
// 获取话题记录
|
||||
let topic = store
|
||||
@ -26,11 +29,6 @@ pub async fn save_topic_to_file(
|
||||
.map_err(|e| format!("Failed to get topic: {}", e))?
|
||||
.ok_or_else(|| "Topic not found".to_string())?;
|
||||
|
||||
// 加载话题消息
|
||||
let messages = store
|
||||
.load_messages_for_topic(topic_id)
|
||||
.map_err(|e| format!("Failed to load messages: {}", e))?;
|
||||
|
||||
// 获取 session 信息(用于系统提示词)
|
||||
let session = store
|
||||
.get_session(&topic.session_id)
|
||||
@ -41,7 +39,7 @@ pub async fn save_topic_to_file(
|
||||
let system_prompt = build_system_prompt(system_prompt_provider, &session, user_message_count);
|
||||
|
||||
// 生成 Markdown 内容
|
||||
let markdown = generate_topic_markdown(&topic, &system_prompt, &messages);
|
||||
let markdown = generate_topic_markdown(&topic, &system_prompt, messages);
|
||||
|
||||
// 确定输出路径
|
||||
let output_path = resolve_topic_filepath(filepath, &topic);
|
||||
@ -156,6 +154,7 @@ fn resolve_topic_filepath(filepath: Option<String>, topic: &TopicRecord) -> Path
|
||||
pub struct SaveTopicCommandHandler {
|
||||
store: Arc<SessionStore>,
|
||||
system_prompt_provider: Arc<dyn SystemPromptProvider>,
|
||||
session_manager: Option<SessionManager>,
|
||||
}
|
||||
|
||||
impl SaveTopicCommandHandler {
|
||||
@ -166,8 +165,14 @@ impl SaveTopicCommandHandler {
|
||||
Self {
|
||||
store,
|
||||
system_prompt_provider,
|
||||
session_manager: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_session_manager(mut self, session_manager: SessionManager) -> Self {
|
||||
self.session_manager = Some(session_manager);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@ -213,7 +218,21 @@ async fn handle_save_topic(
|
||||
.as_deref()
|
||||
.ok_or_else(|| CommandError::new("NO_TOPIC", "No active topic".to_string()))?;
|
||||
|
||||
tracing::debug!(topic_id = %topic_id, "Attempting to save topic");
|
||||
let chat_id = ctx
|
||||
.chat_id
|
||||
.as_deref()
|
||||
.ok_or_else(|| CommandError::new("NO_CHAT_ID", "No chat id".to_string()))?;
|
||||
|
||||
tracing::debug!(topic_id = %topic_id, chat_id = %chat_id, "Attempting to save topic");
|
||||
|
||||
// 从 Session 获取当前 history(包含已压缩的消息)
|
||||
let messages = get_messages_from_session(
|
||||
&handler.session_manager,
|
||||
&ctx.channel_name,
|
||||
chat_id,
|
||||
).await?;
|
||||
|
||||
tracing::debug!(message_count = messages.len(), "Got messages from session");
|
||||
|
||||
// 调用保存函数
|
||||
let output_path = save_topic_to_file(
|
||||
@ -221,16 +240,12 @@ async fn handle_save_topic(
|
||||
filepath,
|
||||
&*handler.store,
|
||||
&*handler.system_prompt_provider,
|
||||
&messages,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| CommandError::new("SAVE_ERROR", e))?;
|
||||
|
||||
// 获取消息数量
|
||||
let message_count = handler
|
||||
.store
|
||||
.load_messages_for_topic(topic_id)
|
||||
.map_err(|e| CommandError::new("LOAD_MESSAGES_ERROR", e.to_string()))?
|
||||
.len();
|
||||
let message_count = messages.len();
|
||||
|
||||
Ok(CommandResponse::success(ctx.request_id)
|
||||
.with_message(
|
||||
|
||||
@ -13,7 +13,6 @@
|
||||
- 当现有工具是完成任务的最直接方式时,优先使用工具。
|
||||
- 除非用户明确要求改变方向,否则保持用户原本目标不变。
|
||||
|
||||
|
||||
## 记忆处理
|
||||
|
||||
### 记忆检索
|
||||
@ -30,8 +29,6 @@
|
||||
- 纯寒暄
|
||||
- 完全不依赖用户历史的直接事实问答
|
||||
|
||||
如果当前请求不明显属于这些例外,就默认先检索。
|
||||
|
||||
|
||||
#### 检索方式
|
||||
- 检索时应提供 queries 数组,数组的数量一般需要10-12个。
|
||||
@ -39,8 +36,6 @@
|
||||
- 越靠近最新会话,生成关键词的比例或者权重应该更高
|
||||
- 例如:queries=['email', '邮件', 'email_folder_preference']
|
||||
|
||||
如果用户在聊持续任务、既有偏好、历史决策、项目上下文、曾经纠正过你的内容,或当前请求看起来像是延续之前的事,优先先搜记忆。
|
||||
|
||||
### 记忆写入
|
||||
|
||||
#### 写入规则
|
||||
@ -58,7 +53,7 @@
|
||||
- 用户说默认xxx的消息
|
||||
|
||||
#### 注意
|
||||
- 如果你决定不再调用工具,则反思一下是否使用 memory_manage
|
||||
- 如果你决定不再调用工具,则反思一下是否使用 memory_manage保存记忆
|
||||
|
||||
## 助理原则
|
||||
|
||||
@ -88,5 +83,7 @@
|
||||
## 定时任务
|
||||
|
||||
- 默认创建静默任务(silent_agent_task),在独立后台会话中执行,不干扰主对话
|
||||
- 仅在用户明确要求时才创建普通任务(agent_task)在主对话中执行
|
||||
- 静默模式下如需发送消息给用户,prompt中需显式使用 send_session_message 工具
|
||||
|
||||
## 注意
|
||||
- 不要通过一次调用写入一个很长的文件,请分段写入
|
||||
|
||||
@ -56,7 +56,9 @@ impl InboundProcessor {
|
||||
command_router.register(Box::new(switch_handler));
|
||||
|
||||
// 注册 get_current 处理器
|
||||
command_router.register(Box::new(GetCurrentSessionCommandHandler::new(store.clone())));
|
||||
command_router.register(Box::new(GetCurrentSessionCommandHandler::new(
|
||||
store.clone(),
|
||||
).with_session_manager(session_manager.clone())));
|
||||
|
||||
// 注册 load_session 处理器
|
||||
command_router.register(Box::new(LoadSessionCommandHandler::new(store.clone())));
|
||||
@ -81,7 +83,7 @@ impl InboundProcessor {
|
||||
command_router.register(Box::new(SaveTopicCommandHandler::new(
|
||||
store.clone(),
|
||||
system_prompt_provider,
|
||||
)));
|
||||
).with_session_manager(session_manager.clone())));
|
||||
|
||||
// 注册 help 处理器(最后注册,获取所有已注册命令的元数据)
|
||||
let metadata = command_router.metadata_arc();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user