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