opensession_core/
extract.rs1use crate::{ContentBlock, EventType, Session};
2
3#[derive(Debug, Clone)]
5pub struct UploadMetadata {
6 pub title: Option<String>,
7 pub description: Option<String>,
8 pub tags: Option<String>,
9 pub created_at: String,
10}
11
12pub fn extract_upload_metadata(session: &Session) -> UploadMetadata {
17 let title = session
18 .context
19 .title
20 .clone()
21 .filter(|t| !t.is_empty())
22 .or_else(|| extract_first_user_text(session).map(|t| truncate_str(&t, 80)));
23
24 let description = session
25 .context
26 .description
27 .clone()
28 .filter(|d| !d.is_empty())
29 .or_else(|| extract_user_texts(session, 3).map(|t| truncate_str(&t, 500)));
30
31 let tags = if session.context.tags.is_empty() {
32 None
33 } else {
34 Some(session.context.tags.join(","))
35 };
36
37 let created_at = session.context.created_at.to_rfc3339();
38
39 UploadMetadata {
40 title,
41 description,
42 tags,
43 created_at,
44 }
45}
46
47pub fn extract_first_user_text(session: &Session) -> Option<String> {
49 for event in &session.events {
50 if matches!(event.event_type, EventType::UserMessage) {
51 for block in &event.content.blocks {
52 if let ContentBlock::Text { text } = block {
53 let trimmed = text.trim();
54 if !trimmed.is_empty() {
55 return Some(trimmed.to_string());
56 }
57 }
58 }
59 }
60 }
61 None
62}
63
64pub fn extract_user_texts(session: &Session, max: usize) -> Option<String> {
66 let mut texts = Vec::new();
67 for event in &session.events {
68 if texts.len() >= max {
69 break;
70 }
71 if matches!(event.event_type, EventType::UserMessage) {
72 for block in &event.content.blocks {
73 if let ContentBlock::Text { text } = block {
74 let trimmed = text.trim();
75 if !trimmed.is_empty() {
76 texts.push(trimmed.to_string());
77 break;
78 }
79 }
80 }
81 }
82 }
83 if texts.is_empty() {
84 None
85 } else {
86 Some(texts.join(" "))
87 }
88}
89
90pub fn truncate_str(s: &str, max_len: usize) -> String {
92 if s.len() <= max_len {
93 s.to_string()
94 } else {
95 let mut end = max_len.saturating_sub(3);
96 while end > 0 && !s.is_char_boundary(end) {
98 end -= 1;
99 }
100 format!("{}...", &s[..end])
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107 use crate::{Agent, Content, Event, Session};
108 use chrono::Utc;
109 use std::collections::HashMap;
110
111 fn make_session(messages: Vec<(&str, EventType)>) -> Session {
112 let mut session = Session::new(
113 "test".to_string(),
114 Agent {
115 provider: "test".to_string(),
116 model: "test".to_string(),
117 tool: "test".to_string(),
118 tool_version: None,
119 },
120 );
121 for (i, (text, event_type)) in messages.into_iter().enumerate() {
122 session.events.push(Event {
123 event_id: format!("e{i}"),
124 timestamp: Utc::now(),
125 event_type,
126 task_id: None,
127 content: Content::text(text),
128 duration_ms: None,
129 attributes: HashMap::new(),
130 });
131 }
132 session
133 }
134
135 #[test]
136 fn test_extract_first_user_text() {
137 let session = make_session(vec![
138 ("hello world", EventType::UserMessage),
139 ("second message", EventType::UserMessage),
140 ]);
141 assert_eq!(
142 extract_first_user_text(&session),
143 Some("hello world".to_string())
144 );
145 }
146
147 #[test]
148 fn test_extract_first_user_text_skips_agent() {
149 let session = make_session(vec![
150 ("agent reply", EventType::AgentMessage),
151 ("user msg", EventType::UserMessage),
152 ]);
153 assert_eq!(
154 extract_first_user_text(&session),
155 Some("user msg".to_string())
156 );
157 }
158
159 #[test]
160 fn test_extract_first_user_text_empty() {
161 let session = make_session(vec![("agent reply", EventType::AgentMessage)]);
162 assert_eq!(extract_first_user_text(&session), None);
163 }
164
165 #[test]
166 fn test_extract_user_texts() {
167 let session = make_session(vec![
168 ("first", EventType::UserMessage),
169 ("reply", EventType::AgentMessage),
170 ("second", EventType::UserMessage),
171 ("third", EventType::UserMessage),
172 ]);
173 assert_eq!(
174 extract_user_texts(&session, 2),
175 Some("first second".to_string())
176 );
177 }
178
179 #[test]
180 fn test_truncate_str_short() {
181 assert_eq!(truncate_str("hello", 10), "hello");
182 }
183
184 #[test]
185 fn test_truncate_str_exact() {
186 assert_eq!(truncate_str("hello", 5), "hello");
187 }
188
189 #[test]
190 fn test_truncate_str_long() {
191 assert_eq!(truncate_str("hello world", 8), "hello...");
192 }
193
194 #[test]
195 fn test_extract_upload_metadata_auto_title() {
196 let session = make_session(vec![
197 ("Build a REST API", EventType::UserMessage),
198 ("Sure, let me help", EventType::AgentMessage),
199 ("Add auth too", EventType::UserMessage),
200 ]);
201 let meta = extract_upload_metadata(&session);
202 assert_eq!(meta.title.as_deref(), Some("Build a REST API"));
203 assert_eq!(
205 meta.description.as_deref(),
206 Some("Build a REST API Add auth too")
207 );
208 assert!(meta.tags.is_none());
209 }
210
211 #[test]
212 fn test_extract_upload_metadata_explicit_title() {
213 let mut session = make_session(vec![("hello", EventType::UserMessage)]);
214 session.context.title = Some("My Title".to_string());
215 session.context.description = Some("My Desc".to_string());
216 session.context.tags = vec!["rust".to_string(), "api".to_string()];
217
218 let meta = extract_upload_metadata(&session);
219 assert_eq!(meta.title.as_deref(), Some("My Title"));
220 assert_eq!(meta.description.as_deref(), Some("My Desc"));
221 assert_eq!(meta.tags.as_deref(), Some("rust,api"));
222 }
223
224 #[test]
225 fn test_extract_upload_metadata_empty_strings() {
226 let mut session = make_session(vec![("hello", EventType::UserMessage)]);
227 session.context.title = Some("".to_string());
228 session.context.description = Some("".to_string());
229
230 let meta = extract_upload_metadata(&session);
231 assert_eq!(meta.title.as_deref(), Some("hello"));
233 assert_eq!(meta.description.as_deref(), Some("hello"));
234 }
235}