1use anyhow::{Result, anyhow};
14use post_cortex_core::core::context_update::{
15 CodeReference, EntityData, EntityRelationship, EntityType, RelationType, UpdateContent,
16 UpdateType,
17};
18use post_cortex_core::core::timeout_utils::with_mcp_timeout;
19use post_cortex_core::services::{
20 BulkUpdateContextRequest as ServiceBulkRequest, PostCortexService,
21 UpdateContextRequest as ServiceUpdateRequest,
22};
23use std::collections::HashMap;
24use tracing::{debug, error, info, instrument, warn};
25use uuid::Uuid;
26
27use crate::{ContextUpdateItem, EntityItem, MCPToolResult, RelationItem, get_service};
28
29fn build_content(
34 interaction_type: &str,
35 content: &HashMap<String, String>,
36) -> Result<(UpdateType, UpdateContent)> {
37 let extract_extras = |exclude_keys: &[&str]| -> Vec<String> {
38 content
39 .iter()
40 .filter(|(k, _)| !exclude_keys.contains(&k.as_str()))
41 .map(|(k, v)| format!("{}: {}", k, v))
42 .collect()
43 };
44
45 let resolve_slot = |preferred: &[&str], fallback_keys: &[&str]| -> String {
46 for k in preferred.iter().chain(fallback_keys.iter()) {
47 if let Some(v) = content.get(*k)
48 && !v.trim().is_empty()
49 {
50 return v.clone();
51 }
52 }
53 String::new()
54 };
55
56 let (update_type, title, description, details, implications) = match interaction_type {
57 "qa" => {
58 let title = resolve_slot(&["question"], &["title"]);
59 let description = resolve_slot(&["answer"], &["description"]);
60 let details = extract_extras(&["question", "answer", "title", "description"]);
61 (
62 UpdateType::QuestionAnswered,
63 title,
64 description,
65 details,
66 vec![],
67 )
68 }
69 "code_change" => {
70 let title = resolve_slot(&["file_path", "file"], &["title", "description"]);
71 let description = resolve_slot(
72 &["changes", "diff", "change_type", "change"],
73 &["description"],
74 );
75 let details = extract_extras(&[
76 "file_path",
77 "file",
78 "title",
79 "description",
80 "changes",
81 "diff",
82 "change_type",
83 "change",
84 ]);
85 (
86 UpdateType::CodeChanged,
87 title,
88 description,
89 details,
90 vec!["Code functionality updated".to_string()],
91 )
92 }
93 "problem_solved" => {
94 let title = resolve_slot(&["problem"], &["title"]);
95 let description = resolve_slot(&["solution"], &["description"]);
96 let details = extract_extras(&["problem", "solution", "title", "description"]);
97 (
98 UpdateType::ProblemSolved,
99 title,
100 description,
101 details,
102 vec!["Problem resolved".to_string()],
103 )
104 }
105 "decision_made" => {
106 let title = resolve_slot(&["decision"], &["title"]);
107 let description = resolve_slot(&["rationale"], &["description"]);
108 let details = extract_extras(&["decision", "rationale", "title", "description"]);
109 (
110 UpdateType::DecisionMade,
111 title,
112 description,
113 details,
114 vec![],
115 )
116 }
117 "requirement_added" => {
118 let title = resolve_slot(&["requirement"], &["title"]);
119 let description = resolve_slot(&["description"], &[]);
120 let details = extract_extras(&["requirement", "priority", "title", "description"]);
121 (
122 UpdateType::RequirementAdded,
123 title,
124 description,
125 details,
126 vec![],
127 )
128 }
129 "concept_defined" => {
130 let title = resolve_slot(&["concept"], &["title"]);
131 let description = resolve_slot(&["definition"], &["description"]);
132 let details = extract_extras(&["concept", "definition", "title", "description"]);
133 (
134 UpdateType::ConceptDefined,
135 title,
136 description,
137 details,
138 vec![],
139 )
140 }
141 other => return Err(anyhow!("Unknown interaction type: {}", other)),
142 };
143
144 Ok((
145 update_type,
146 UpdateContent {
147 title,
148 description,
149 details,
150 examples: vec![],
151 implications,
152 },
153 ))
154}
155
156fn parse_entity_type(s: &str) -> EntityType {
159 match s.to_lowercase().as_str() {
160 "technology" => EntityType::Technology,
161 "concept" => EntityType::Concept,
162 "problem" => EntityType::Problem,
163 "solution" => EntityType::Solution,
164 "decision" => EntityType::Decision,
165 "code_component" | "codecomponent" => EntityType::CodeComponent,
166 _ => EntityType::Concept,
167 }
168}
169
170fn parse_relation_type(s: &str) -> Option<RelationType> {
174 match s.to_lowercase().as_str() {
175 "required_by" | "requiredby" => Some(RelationType::RequiredBy),
176 "leads_to" | "leadsto" => Some(RelationType::LeadsTo),
177 "related_to" | "relatedto" => Some(RelationType::RelatedTo),
178 "conflicts_with" | "conflictswith" => Some(RelationType::ConflictsWith),
179 "depends_on" | "dependson" => Some(RelationType::DependsOn),
180 "implements" => Some(RelationType::Implements),
181 "caused_by" | "causedby" => Some(RelationType::CausedBy),
182 "solves" => Some(RelationType::Solves),
183 _ => None,
184 }
185}
186
187fn entities_to_domain(items: &[EntityItem]) -> Vec<EntityData> {
188 let now = chrono::Utc::now();
189 items
190 .iter()
191 .map(|e| EntityData {
192 name: e.name.clone(),
193 entity_type: parse_entity_type(&e.entity_type),
194 first_mentioned: now,
195 last_mentioned: now,
196 mention_count: 1,
197 importance_score: 1.0,
198 description: None,
199 })
200 .collect()
201}
202
203fn relations_to_domain(items: &[RelationItem]) -> Result<Vec<EntityRelationship>> {
204 let mut out = Vec::with_capacity(items.len());
205 for (i, r) in items.iter().enumerate() {
206 let rt = parse_relation_type(&r.relation_type).ok_or_else(|| {
207 anyhow!(
208 "relation[{i}]: unknown relation_type {:?}; valid values are: \
209 required_by, leads_to, related_to, conflicts_with, depends_on, implements, caused_by, solves",
210 r.relation_type
211 )
212 })?;
213 out.push(EntityRelationship {
214 from_entity: r.from_entity.clone(),
215 to_entity: r.to_entity.clone(),
216 relation_type: rt,
217 context: r.context.clone(),
218 });
219 }
220 Ok(out)
221}
222
223fn build_request(
225 session_id: Uuid,
226 interaction_type: &str,
227 content: &HashMap<String, String>,
228 entities: &[EntityItem],
229 relations: &[RelationItem],
230 code_reference: Option<CodeReference>,
231) -> Result<ServiceUpdateRequest> {
232 let (update_type, update_content) = build_content(interaction_type, content)?;
233 Ok(ServiceUpdateRequest {
234 session_id,
235 interaction_type: update_type,
236 content: update_content,
237 entities: entities_to_domain(entities),
238 relations: relations_to_domain(relations)?,
239 code_reference,
240 })
241}
242
243#[instrument(skip(content, entities, relations), fields(
246 session_id = %session_id,
247 interaction_type = %interaction_type,
248 entities_count = entities.len(),
249 relations_count = relations.len(),
250 has_code_reference = code_reference.is_some()
251))]
252pub async fn update_conversation_context(
253 interaction_type: String,
254 content: HashMap<String, String>,
255 entities: Vec<EntityItem>,
256 relations: Vec<RelationItem>,
257 code_reference: Option<CodeReference>,
258 session_id: Uuid,
259) -> Result<MCPToolResult> {
260 info!("MCP-TOOLS: update_conversation_context() called");
261 let service = get_service().await?;
262
263 let req = match build_request(
264 session_id,
265 &interaction_type,
266 &content,
267 &entities,
268 &relations,
269 code_reference,
270 ) {
271 Ok(r) => r,
272 Err(e) => {
273 error!("update_conversation_context: bad input — {}", e);
274 return Ok(MCPToolResult::error(e.to_string()));
275 }
276 };
277
278 let result = with_mcp_timeout(async {
279 match service.update_context(req).await {
280 Ok(resp) => {
281 debug!(
282 "update_conversation_context: persisted entry {} in session {}",
283 resp.entry_id, resp.session_id
284 );
285 Ok(MCPToolResult::success(
286 "Context updated successfully".to_string(),
287 None,
288 ))
289 }
290 Err(e) => {
291 warn!("update_conversation_context: service rejected — {}", e);
292 Ok(MCPToolResult::error(e.to_string()))
293 }
294 }
295 })
296 .await;
297
298 match result {
299 Ok(r) => r,
300 Err(timeout_error) => {
301 error!(
302 "TIMEOUT: update_conversation_context — session: {}, error: {}",
303 session_id, timeout_error
304 );
305 Ok(MCPToolResult::error(format!(
306 "Operation timed out: {}",
307 timeout_error
308 )))
309 }
310 }
311}
312
313pub async fn bulk_update_conversation_context(
318 updates: Vec<ContextUpdateItem>,
319 session_id: Uuid,
320) -> Result<MCPToolResult> {
321 info!(
322 "MCP-TOOLS: bulk_update_conversation_context() called with {} updates for session {}",
323 updates.len(),
324 session_id
325 );
326
327 let service = get_service().await?;
328
329 let mut requests = Vec::with_capacity(updates.len());
330 let mut error_count = 0;
331 let mut errors: Vec<String> = Vec::new();
332 for (index, item) in updates.iter().enumerate() {
333 match build_request(
334 session_id,
335 &item.interaction_type,
336 &item.content,
337 &item.entities,
338 &item.relations,
339 item.code_reference.clone(),
340 ) {
341 Ok(req) => requests.push(req),
342 Err(e) => {
343 error_count += 1;
344 errors.push(format!("Update {}: {}", index, e));
345 }
346 }
347 }
348
349 let success_count = if errors.is_empty() {
353 match service
354 .bulk_update_context(ServiceBulkRequest {
355 session_id,
356 updates: requests,
357 })
358 .await
359 {
360 Ok(resp) => resp.entry_ids.len(),
361 Err(e) => {
362 errors.push(format!("Bulk persist failed: {}", e));
363 error_count += 1;
364 0
365 }
366 }
367 } else {
368 let mut count = 0;
372 for (offset, req) in requests.into_iter().enumerate() {
373 match service.update_context(req).await {
374 Ok(_) => count += 1,
375 Err(e) => {
376 error_count += 1;
377 errors.push(format!("Update (translated index {offset}): {}", e));
378 }
379 }
380 }
381 count
382 };
383
384 let message = if error_count == 0 {
385 format!(
386 "Bulk update completed successfully: {} updates added",
387 success_count
388 )
389 } else {
390 format!(
391 "Bulk update completed with errors: {} succeeded, {} failed",
392 success_count, error_count
393 )
394 };
395
396 Ok(MCPToolResult::success(
397 message,
398 Some(serde_json::json!({
399 "success_count": success_count,
400 "error_count": error_count,
401 "errors": errors,
402 })),
403 ))
404}