offline_intelligence/api/
conversation_api.rs1use axum::{
4 extract::{State, Path},
5 response::{IntoResponse, Response},
6 Json,
7};
8use axum::http::StatusCode;
9use serde::{Deserialize, Serialize};
10use serde_json::Value;
11use tracing::{info, error};
12
13use crate::shared_state::UnifiedAppState;
14
15#[derive(Debug, Serialize)]
17pub struct ConversationsResponse {
18 pub conversations: Vec<ConversationSummary>,
19}
20
21#[derive(Debug, Serialize)]
23pub struct ConversationSummary {
24 pub id: String,
25 pub title: String,
26 pub created_at: String,
27 pub last_accessed: String,
28 pub message_count: usize,
29 pub pinned: bool,
30}
31
32#[derive(Debug, Serialize)]
34pub struct ConversationDetailResponse {
35 pub id: String,
36 pub title: String,
37 pub messages: Vec<MessageResponse>,
38}
39
40#[derive(Debug, Serialize)]
42pub struct MessageResponse {
43 pub role: String,
44 pub content: String,
45}
46
47pub async fn get_conversations(
49 State(state): State<UnifiedAppState>,
50) -> Result<Json<ConversationsResponse>, Response> {
51 info!("Fetching all conversations");
52
53 let orchestrator_lock = state.context_orchestrator.read().await;
54
55 if let Some(ref orchestrator) = *orchestrator_lock {
56 match orchestrator.database().conversations.get_all_sessions() {
57 Ok(sessions) => {
58 let mut conversations = Vec::new();
59
60 for session in sessions {
61 if let Some(ref title) = session.metadata.title {
64 let message_count = orchestrator.database().conversations
66 .get_session_message_count(&session.id)
67 .unwrap_or(0);
68
69 conversations.push(ConversationSummary {
70 id: session.id.clone(),
71 title: title.clone(),
72 created_at: session.created_at.to_rfc3339(),
73 last_accessed: session.last_accessed.to_rfc3339(),
74 message_count,
75 pinned: session.metadata.pinned,
76 });
77 }
78 }
79
80 info!("Found {} conversations", conversations.len());
81 Ok(Json(ConversationsResponse { conversations }))
82 }
83 Err(e) => {
84 error!("Failed to fetch conversations: {}", e);
85 Err((StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)).into_response())
86 }
87 }
88 } else {
89 error!("Context orchestrator not initialized");
90 Err((StatusCode::SERVICE_UNAVAILABLE, "Memory system not available").into_response())
91 }
92}
93
94pub async fn get_conversation(
96 State(state): State<UnifiedAppState>,
97 Path(session_id): Path<String>,
98) -> Result<Json<ConversationDetailResponse>, Response> {
99 info!("Fetching conversation: {}", session_id);
100
101 let orchestrator_lock = state.context_orchestrator.read().await;
102
103 if let Some(ref orchestrator) = *orchestrator_lock {
104 let session = match orchestrator.database().conversations.get_session(&session_id) {
106 Ok(Some(s)) => s,
107 Ok(None) => {
108 return Err((StatusCode::NOT_FOUND, "Conversation not found").into_response());
109 }
110 Err(e) => {
111 error!("Failed to fetch session: {}", e);
112 return Err((StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)).into_response());
113 }
114 };
115
116 let messages = match orchestrator.database().conversations.get_session_messages(&session_id, None, None) {
118 Ok(msgs) => msgs.into_iter()
119 .map(|msg| MessageResponse {
120 role: msg.role,
121 content: msg.content,
122 })
123 .collect(),
124 Err(e) => {
125 error!("Failed to fetch messages: {}", e);
126 return Err((StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)).into_response());
127 }
128 };
129
130 Ok(Json(ConversationDetailResponse {
131 id: session.id,
132 title: session.metadata.title.unwrap_or_else(|| "New Chat".to_string()),
133 messages,
134 }))
135 } else {
136 error!("Context orchestrator not initialized");
137 Err((StatusCode::SERVICE_UNAVAILABLE, "Memory system not available").into_response())
138 }
139}
140
141#[derive(Debug, Deserialize)]
143pub struct UpdateTitleRequest {
144 pub title: String,
145}
146
147pub async fn update_conversation_title(
149 State(state): State<UnifiedAppState>,
150 Path(session_id): Path<String>,
151 Json(req): Json<UpdateTitleRequest>,
152) -> Result<Json<Value>, Response> {
153 info!("Updating title for conversation: {}", session_id);
154
155 if req.title.is_empty() {
156 return Err((StatusCode::BAD_REQUEST, "Title cannot be empty").into_response());
157 }
158
159 let orchestrator_lock = state.context_orchestrator.read().await;
160
161 if let Some(ref orchestrator) = *orchestrator_lock {
162 match orchestrator.database().conversations.update_session_title(&session_id, &req.title) {
163 Ok(_) => {
164 info!("Successfully updated title for conversation: {}", session_id);
165 Ok(Json(serde_json::json!({
166 "success": true,
167 "id": session_id,
168 "title": req.title
169 })))
170 }
171 Err(e) => {
172 error!("Failed to update conversation title for session {}: {}", session_id, e);
174 Err((StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)).into_response())
175 }
176 }
177 } else {
178 error!("Context orchestrator not initialized");
179 Err((StatusCode::SERVICE_UNAVAILABLE, "Memory system not available").into_response())
180 }
181}
182
183pub async fn delete_conversation(
187 State(state): State<UnifiedAppState>,
188 Path(session_id): Path<String>,
189) -> Result<Json<Value>, Response> {
190 info!("Deleting conversation: {}", session_id);
191
192 let orchestrator_lock = state.context_orchestrator.read().await;
193
194 if let Some(ref orchestrator) = *orchestrator_lock {
195 match orchestrator.database().conversations.delete_session(&session_id) {
196 Ok(deleted_count) => {
197 if deleted_count == 0 {
198 info!("Conversation not found for deletion: {}", session_id);
199 Err((StatusCode::NOT_FOUND, format!("Conversation not found: {}", session_id)).into_response())
200 } else {
201 info!("Successfully deleted conversation: {}", session_id);
202 Ok(Json(serde_json::json!({
203 "success": true,
204 "id": session_id
205 })))
206 }
207 }
208 Err(e) => {
209 error!("Failed to delete conversation: {}", e);
210 Err((StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)).into_response())
212 }
213 }
214 } else {
215 error!("Context orchestrator not initialized");
216 Err((StatusCode::SERVICE_UNAVAILABLE, "Memory system not available").into_response())
217 }
218}
219
220#[derive(Debug, Deserialize)]
222pub struct UpdatePinnedRequest {
223 pub pinned: bool,
224}
225
226pub async fn update_conversation_pinned(
228 State(state): State<UnifiedAppState>,
229 Path(session_id): Path<String>,
230 Json(req): Json<UpdatePinnedRequest>,
231) -> Result<Json<Value>, Response> {
232 info!("Updating pinned status for conversation: {} to {}", session_id, req.pinned);
233
234 let orchestrator_lock = state.context_orchestrator.read().await;
235
236 if let Some(ref orchestrator) = *orchestrator_lock {
237 match orchestrator.database().conversations.update_session_pinned(&session_id, req.pinned) {
238 Ok(_) => {
239 info!("Successfully updated pinned status for conversation: {}", session_id);
240 Ok(Json(serde_json::json!({
241 "success": true,
242 "id": session_id,
243 "pinned": req.pinned
244 })))
245 }
246 Err(e) => {
247 let error_msg = e.to_string();
248 if error_msg.contains("not found") {
250 error!("Conversation not found: {}", session_id);
251 Err((StatusCode::NOT_FOUND, format!("Conversation not found: {}", session_id)).into_response())
252 } else {
253 error!("Failed to update conversation pinned status: {}", e);
254 Err((StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)).into_response())
255 }
256 }
257 }
258 } else {
259 error!("Context orchestrator not initialized");
260 Err((StatusCode::SERVICE_UNAVAILABLE, "Memory system not available").into_response())
261 }
262}