use chrono::Utc;
use std::path::{Path, PathBuf};
use tokio::fs;
use super::rich::RichAgentContext;
use crate::agent::components::memory::FileBasedStorage;
use mofa_kernel::agent::context::AgentContext;
use mofa_kernel::agent::error::{AgentError, AgentResult};
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct AgentIdentity {
pub name: String,
pub description: String,
pub icon: Option<String>,
}
impl Default for AgentIdentity {
fn default() -> Self {
Self {
name: "Agent".to_string(),
description: "A helpful AI assistant".to_string(),
icon: None,
}
}
}
pub struct PromptContext {
workspace: PathBuf,
identity: AgentIdentity,
bootstrap_files: Vec<String>,
memory: Option<Arc<FileBasedStorage>>,
always_load: Vec<String>,
agent_name: String,
rich_ctx: RichAgentContext,
}
impl PromptContext {
pub fn default_bootstrap_files() -> Vec<String> {
vec![
"AGENTS.md".to_string(),
"SOUL.md".to_string(),
"USER.md".to_string(),
"TOOLS.md".to_string(),
"IDENTITY.md".to_string(),
]
}
pub async fn new(workspace: impl AsRef<Path>) -> AgentResult<Self> {
let workspace = workspace.as_ref().to_path_buf();
let core_ctx = AgentContext::new(format!("prompt-{}", uuid::Uuid::new_v4()));
let rich_ctx = RichAgentContext::new(core_ctx);
Ok(Self {
workspace,
identity: AgentIdentity::default(),
bootstrap_files: Self::default_bootstrap_files(),
memory: None,
always_load: Vec::new(),
agent_name: "agent".to_string(),
rich_ctx,
})
}
pub async fn with_identity(
workspace: impl AsRef<Path>,
identity: AgentIdentity,
) -> AgentResult<Self> {
let workspace = workspace.as_ref().to_path_buf();
let agent_name = identity.name.clone();
let core_ctx = AgentContext::new(format!("prompt-{}", uuid::Uuid::new_v4()));
let rich_ctx = RichAgentContext::new(core_ctx);
Ok(Self {
workspace,
identity,
bootstrap_files: Self::default_bootstrap_files(),
memory: None,
always_load: Vec::new(),
agent_name,
rich_ctx,
})
}
pub fn with_bootstrap_files(mut self, files: Vec<String>) -> Self {
self.bootstrap_files = files;
self
}
pub fn with_always_load(mut self, skills: Vec<String>) -> Self {
self.always_load = skills;
self
}
async fn init_memory(&mut self) -> AgentResult<()> {
if self.memory.is_none() {
self.memory = Some(Arc::new(
FileBasedStorage::new(&self.workspace).await.map_err(|e| {
AgentError::MemoryError(format!("Failed to init memory: {}", e))
})?,
));
}
Ok(())
}
pub async fn build_system_prompt(&mut self) -> AgentResult<String> {
let mut parts = Vec::new();
parts.push(self.get_identity_section());
let bootstrap = self.load_bootstrap_files().await?;
if !bootstrap.is_empty() {
parts.push(bootstrap);
}
if let Err(_) = self.init_memory().await {
} else if let Some(memory) = &self.memory
&& let Ok(memory_context) = memory.get_memory_context().await
&& !memory_context.is_empty()
{
parts.push(format!("# Memory\n\n{}", memory_context));
}
self.rich_ctx
.record_output(
"prompt_builder",
serde_json::json!({
"prompt_length": parts.join("\n\n---\n\n").len(),
"bootstrap_files": self.bootstrap_files.len(),
}),
)
.await;
Ok(parts.join("\n\n---\n\n"))
}
fn get_identity_section(&self) -> String {
let now = Utc::now().format("%Y-%m-%d %H:%M (%A)");
let workspace_path = self.workspace.display();
let icon = self.identity.icon.as_deref().unwrap_or("");
let description = if self.identity.description.is_empty() {
"a helpful AI assistant"
} else {
&self.identity.description
};
format!(
r#"# {} {} {}
You are {}, {}.
## Current Time
{}
## Workspace
Your workspace is at: {}
- Memory files: {}/memory/MEMORY.md
- Daily notes: {}/memory/YYYY-MM-DD.md
- Custom skills: {}/skills/{{{{skill-name}}}}/SKILL.md
Always be helpful, accurate, and concise. When using tools, explain what you're doing.
When remembering something, write to {}/memory/MEMORY.md"#,
icon,
self.identity.name,
description,
self.identity.name,
description,
now,
workspace_path,
workspace_path,
workspace_path,
workspace_path,
workspace_path
)
}
async fn load_bootstrap_files(&self) -> AgentResult<String> {
let mut parts = Vec::new();
for filename in &self.bootstrap_files {
let file_path = self.workspace.join(filename);
if file_path.exists()
&& let Ok(content) = fs::read_to_string(&file_path).await
{
parts.push(format!("## {}\n\n{}", filename, content));
}
}
Ok(parts.join("\n\n"))
}
pub async fn memory(&mut self) -> AgentResult<&FileBasedStorage> {
self.init_memory().await?;
Ok(self.memory.as_ref().unwrap())
}
pub fn workspace(&self) -> &Path {
&self.workspace
}
pub fn rich_context(&self) -> &RichAgentContext {
&self.rich_ctx
}
pub fn identity(&self) -> &AgentIdentity {
&self.identity
}
}
pub struct PromptContextBuilder {
workspace: PathBuf,
identity: AgentIdentity,
bootstrap_files: Vec<String>,
always_load: Vec<String>,
}
impl PromptContextBuilder {
pub fn new(workspace: impl AsRef<Path>) -> Self {
Self {
workspace: workspace.as_ref().to_path_buf(),
identity: AgentIdentity::default(),
bootstrap_files: PromptContext::default_bootstrap_files(),
always_load: Vec::new(),
}
}
pub fn with_identity(mut self, identity: AgentIdentity) -> Self {
self.identity = identity;
self
}
pub fn with_name(mut self, name: impl Into<String>) -> Self {
self.identity.name = name.into();
self
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.identity.description = description.into();
self
}
pub fn with_icon(mut self, icon: impl Into<String>) -> Self {
self.identity.icon = Some(icon.into());
self
}
pub fn with_bootstrap_files(mut self, files: Vec<String>) -> Self {
self.bootstrap_files = files;
self
}
pub fn with_always_load(mut self, skills: Vec<String>) -> Self {
self.always_load = skills;
self
}
pub async fn build(self) -> AgentResult<PromptContext> {
let agent_name = self.identity.name.clone();
PromptContext::with_identity(&self.workspace, self.identity)
.await
.map(|mut ctx| {
ctx.bootstrap_files = self.bootstrap_files;
ctx.always_load = self.always_load;
ctx.agent_name = agent_name;
ctx
})
}
}
impl Clone for PromptContext {
fn clone(&self) -> Self {
Self {
workspace: self.workspace.clone(),
identity: self.identity.clone(),
bootstrap_files: self.bootstrap_files.clone(),
memory: self.memory.clone(),
always_load: self.always_load.clone(),
agent_name: self.agent_name.clone(),
rich_ctx: RichAgentContext::new(self.rich_ctx.inner().clone()),
}
}
}