use anyhow::Result;
use async_trait::async_trait;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileContext {
pub thread_id: String,
pub task_id: Option<String>,
pub tool_call_id: Option<String>,
pub content_type: Option<String>,
pub original_filename: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct FileMetadata {
pub file_id: String,
pub relative_path: String,
pub size: u64,
pub content_type: Option<String>,
pub original_filename: Option<String>,
#[schemars(with = "String")]
pub created_at: chrono::DateTime<chrono::Utc>,
#[schemars(with = "String")]
pub updated_at: chrono::DateTime<chrono::Utc>,
pub checksum: Option<String>,
pub stats: Option<FileStats>,
pub preview: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct Artifact {
pub file_metadata: FileMetadata,
pub thread_id: String,
pub task_id: Option<String>,
pub tool_call_id: Option<String>,
}
impl FileMetadata {
pub fn display_name(&self) -> String {
self.original_filename
.clone()
.unwrap_or_else(|| format!("file_{}", &self.file_id[..8]))
}
pub fn size_display(&self) -> String {
let size = self.size as f64;
if size < 1024.0 {
format!("{}B", self.size)
} else if size < 1024.0 * 1024.0 {
format!("{:.1}KB", size / 1024.0)
} else if size < 1024.0 * 1024.0 * 1024.0 {
format!("{:.1}MB", size / (1024.0 * 1024.0))
} else {
format!("{:.1}GB", size / (1024.0 * 1024.0 * 1024.0))
}
}
pub fn is_text_file(&self) -> bool {
self.content_type
.as_ref()
.map(|ct| ct.starts_with("text/") || ct.contains("json") || ct.contains("xml"))
.unwrap_or(false)
}
pub fn summary(&self) -> String {
format!(
"{} ({}{})",
self.display_name(),
self.size_display(),
if let Some(ct) = &self.content_type {
format!(", {}", ct)
} else {
String::new()
}
)
}
}
impl Artifact {
pub fn new(
file_metadata: FileMetadata,
thread_id: String,
task_id: Option<String>,
tool_call_id: Option<String>,
) -> Self {
Self {
file_metadata,
thread_id,
task_id,
tool_call_id,
}
}
pub fn artifact_path(&self) -> String {
if let Some(task_id) = &self.task_id {
format!(
"{}/artifact/{}/{}",
self.thread_id, task_id, self.file_metadata.file_id
)
} else {
format!("{}/artifact/{}", self.thread_id, self.file_metadata.file_id)
}
}
pub fn display_name(&self) -> String {
self.file_metadata.display_name()
}
pub fn size_display(&self) -> String {
self.file_metadata.size_display()
}
pub fn summary(&self) -> String {
self.file_metadata.summary()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Hash)]
pub struct ArtifactNamespace {
pub thread_id: String,
pub task_id: Option<String>,
}
impl ArtifactNamespace {
pub fn new(thread_id: String, task_id: Option<String>) -> Self {
Self { thread_id, task_id }
}
fn short_hex(id: &str) -> String {
let mut hasher = DefaultHasher::new();
id.hash(&mut hasher);
format!("{:08x}", hasher.finish())
}
pub fn thread_path(&self) -> String {
let short_thread = Self::short_hex(&self.thread_id);
format!("threads/{}", short_thread)
}
pub fn task_path(&self) -> Option<String> {
self.task_id.as_ref().map(|task_id| {
let short_thread = Self::short_hex(&self.thread_id);
let short_task = Self::short_hex(task_id);
format!("threads/{}/tasks/{}", short_thread, short_task)
})
}
pub fn primary_path(&self) -> String {
self.task_path().unwrap_or_else(|| self.thread_path())
}
pub fn all_paths(&self) -> Vec<String> {
let mut paths = vec![self.thread_path()];
if let Some(task_path) = self.task_path() {
paths.push(task_path);
}
paths
}
pub fn from_path(_path: &str) -> Option<Self> {
None
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum FileStats {
Json(JsonStats),
Markdown(MarkdownStats),
Text(TextStats),
}
impl FileStats {
pub fn stats_type(&self) -> &'static str {
match self {
FileStats::Json(_) => "json",
FileStats::Markdown(_) => "markdown",
FileStats::Text(_) => "text",
}
}
pub fn summary(&self) -> String {
match self {
FileStats::Json(stats) => stats.summary(),
FileStats::Markdown(stats) => stats.summary(),
FileStats::Text(stats) => stats.summary(),
}
}
pub fn context_info(&self) -> String {
match self {
FileStats::Json(stats) => stats.context_info(),
FileStats::Markdown(stats) => stats.context_info(),
FileStats::Text(stats) => stats.context_info(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema)]
pub struct JsonStats {
pub is_array: bool,
pub array_length: Option<usize>,
pub top_level_keys: Vec<String>,
pub nested_depth: usize,
pub unique_values_sample: HashMap<String, Vec<String>>,
pub cardinality_estimates: HashMap<String, usize>,
pub preview: String,
}
impl JsonStats {
pub fn summary(&self) -> String {
if self.is_array {
format!(
"JSON array with {} elements, {} unique keys, depth {}",
self.array_length.unwrap_or(0),
self.top_level_keys.len(),
self.nested_depth
)
} else {
format!(
"JSON object with {} keys, depth {}",
self.top_level_keys.len(),
self.nested_depth
)
}
}
pub fn context_info(&self) -> String {
let mut info = self.summary();
if !self.top_level_keys.is_empty() {
info.push_str(&format!("\nKeys: {}", self.top_level_keys.join(", ")));
}
let high_card_fields: Vec<_> = self
.cardinality_estimates
.iter()
.filter(|&(_, &count)| count > 50)
.map(|(field, count)| format!("{} (~{})", field, count))
.collect();
if !high_card_fields.is_empty() {
info.push_str(&format!(
"\nHigh-cardinality fields: {}",
high_card_fields.join(", ")
));
}
for (field, values) in &self.unique_values_sample {
if values.len() <= 10 {
info.push_str(&format!("\n{}: {}", field, values.join(", ")));
}
}
info
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema)]
pub struct MarkdownStats {
pub word_count: usize,
pub headings: Vec<HeadingInfo>,
pub code_blocks: usize,
pub links: usize,
pub images: usize,
pub tables: usize,
pub lists: usize,
pub front_matter: Option<String>,
pub preview: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema)]
pub struct HeadingInfo {
pub text: String,
pub level: usize,
}
impl MarkdownStats {
pub fn summary(&self) -> String {
format!(
"Markdown: {} words, {} headings, {} code blocks, {} tables",
self.word_count,
self.headings.len(),
self.code_blocks,
self.tables
)
}
pub fn context_info(&self) -> String {
let mut info = self.summary();
if !self.headings.is_empty() {
info.push_str("\nStructure:");
for heading in &self.headings[..5.min(self.headings.len())] {
let indent = " ".repeat(heading.level.saturating_sub(1));
info.push_str(&format!("\n{}{}", indent, heading.text));
}
}
if let Some(fm_type) = &self.front_matter {
info.push_str(&format!("\nFrontmatter: {}", fm_type));
}
info
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema)]
pub struct TextStats {
pub lines: usize,
pub words: usize,
pub characters: usize,
pub encoding: String,
pub language: Option<String>,
pub structure_hints: TextStructure,
pub preview: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum TextStructure {
LogFile {
log_level_counts: HashMap<String, usize>,
},
ConfigFile {
format: String,
},
CodeFile {
language: String,
function_count: usize,
},
PlainText,
}
impl TextStats {
pub fn summary(&self) -> String {
format!(
"Text: {} lines, {} words ({} chars)",
self.lines, self.words, self.characters
)
}
pub fn context_info(&self) -> String {
let mut info = self.summary();
if let Some(lang) = &self.language {
info.push_str(&format!("\nLanguage: {}", lang));
}
match &self.structure_hints {
TextStructure::LogFile { log_level_counts } => {
info.push_str("\nStructure: Log file");
let levels: Vec<_> = log_level_counts
.iter()
.map(|(level, count)| format!("{}: {}", level, count))
.collect();
if !levels.is_empty() {
info.push_str(&format!("\nLevels: {}", levels.join(", ")));
}
}
TextStructure::ConfigFile { format } => {
info.push_str(&format!("\nStructure: Config file ({})", format));
}
TextStructure::CodeFile {
language,
function_count,
} => {
info.push_str(&format!(
"\nStructure: Code file ({}, {} functions)",
language, function_count
));
}
TextStructure::PlainText => {
info.push_str("\nStructure: Plain text");
}
}
info
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ReadParams {
pub start_line: Option<u64>,
pub end_line: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileReadResult {
pub content: String,
pub start_line: u64,
pub end_line: u64,
pub total_lines: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DirectoryListing {
pub path: String,
pub entries: Vec<DirectoryEntry>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DirectoryEntry {
pub name: String,
pub is_file: bool,
pub is_dir: bool,
pub size: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchResult {
pub path: String,
pub matches: Vec<SearchMatch>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchMatch {
pub file_path: String,
pub line_number: Option<u64>,
pub line_content: String,
pub match_text: String,
}
#[async_trait]
pub trait FileSystemOps: Send + Sync + std::fmt::Debug {
async fn read(&self, path: &str, params: ReadParams) -> Result<FileReadResult>;
async fn read_raw(&self, path: &str) -> Result<String> {
let result = self.read(path, ReadParams::default()).await?;
if result.content.contains("→") {
Ok(result
.content
.lines()
.map(|line| {
if let Some(pos) = line.find("→") {
&line[pos + 1..]
} else {
line
}
})
.collect::<Vec<_>>()
.join("\n"))
} else {
Ok(result.content)
}
}
async fn read_with_line_numbers(
&self,
path: &str,
params: ReadParams,
) -> Result<FileReadResult> {
self.read(path, params).await
}
async fn write(&self, path: &str, content: &str) -> Result<()>;
async fn list(&self, path: &str) -> Result<DirectoryListing>;
async fn delete(&self, path: &str, recursive: bool) -> Result<()>;
async fn search(
&self,
path: &str,
content_pattern: Option<&str>,
file_pattern: Option<&str>,
) -> Result<SearchResult>;
async fn copy(&self, from: &str, to: &str) -> Result<()>;
async fn move_file(&self, from: &str, to: &str) -> Result<()>;
async fn mkdir(&self, path: &str) -> Result<()>;
async fn info(&self, path: &str) -> Result<FileMetadata>;
async fn tree(&self, path: &str) -> Result<DirectoryListing>;
}