Skip to main content

post_cortex_mcp/
analysis.rs

1//! Analysis, summaries, insights, and session statistics.
2
3use crate::{MCPToolResult, get_memory_system};
4use anyhow::Result;
5use post_cortex_core::summary::SummaryGenerator;
6use uuid::Uuid;
7
8/// Generate a structured summary of the session with optional filtering.
9pub async fn get_structured_summary(
10    session_id: String,
11    decisions_limit: Option<usize>,
12    entities_limit: Option<usize>,
13    questions_limit: Option<usize>,
14    concepts_limit: Option<usize>,
15    min_confidence: Option<f32>,
16    compact: Option<bool>,
17) -> Result<MCPToolResult> {
18    let uuid =
19        Uuid::parse_str(&session_id).map_err(|e| anyhow::anyhow!("Invalid session ID: {}", e))?;
20
21    let system = get_memory_system().await?;
22    let session_arc = system
23        .get_session(uuid)
24        .await
25        .map_err(|e| anyhow::anyhow!("Failed to load session: {}", e))?;
26
27    let session = session_arc.load();
28
29    use post_cortex_core::summary::SummaryOptions;
30    let user_requested_compact = compact.unwrap_or(false);
31    const MAX_TOKENS: usize = 50_000;
32
33    let (estimated_tokens, should_compact) =
34        SummaryGenerator::estimate_summary_size(&session, MAX_TOKENS);
35    let auto_compacted = !user_requested_compact && should_compact;
36
37    if auto_compacted {
38        log::info!(
39            "Pre-estimated {} tokens > {} max. Using compact mode directly.",
40            estimated_tokens,
41            MAX_TOKENS
42        );
43    }
44
45    let options = if user_requested_compact || auto_compacted {
46        SummaryOptions::compact()
47    } else {
48        SummaryOptions {
49            decisions_limit,
50            entities_limit,
51            questions_limit,
52            concepts_limit,
53            min_confidence,
54            compact: false,
55        }
56    };
57
58    let summary = SummaryGenerator::generate_structured_summary_filtered(&session, &options);
59
60    let message = if user_requested_compact {
61        "Generated compact structured summary".to_string()
62    } else if auto_compacted {
63        format!(
64            "Auto-compacted summary (was too large for MCP). Showing: {} decisions, {} entities, {} questions, {} concepts",
65            summary.key_decisions.len(),
66            summary.entity_summaries.len(),
67            summary.open_questions.len(),
68            summary.key_concepts.len()
69        )
70    } else {
71        format!(
72            "Generated structured summary (decisions: {}, entities: {}, questions: {}, concepts: {})",
73            summary.key_decisions.len(),
74            summary.entity_summaries.len(),
75            summary.open_questions.len(),
76            summary.key_concepts.len()
77        )
78    };
79
80    Ok(MCPToolResult::success(
81        message,
82        Some(serde_json::to_value(summary)?),
83    ))
84}
85
86/// Extract a timeline of key architectural and technical decisions.
87pub async fn get_key_decisions(session_id: String) -> Result<MCPToolResult> {
88    let uuid =
89        Uuid::parse_str(&session_id).map_err(|e| anyhow::anyhow!("Invalid session ID: {}", e))?;
90
91    let system = get_memory_system().await?;
92    let session_arc = system
93        .get_session(uuid)
94        .await
95        .map_err(|e| anyhow::anyhow!("Failed to load session: {}", e))?;
96
97    let session = session_arc.load();
98    let decisions = SummaryGenerator::extract_decision_timeline(&session);
99
100    Ok(MCPToolResult::success(
101        format!("Found {} key decisions", decisions.len()),
102        Some(serde_json::to_value(decisions)?),
103    ))
104}
105
106/// Extract the top insights from a session ranked by importance.
107pub async fn get_key_insights(session_id: String, limit: Option<usize>) -> Result<MCPToolResult> {
108    let uuid =
109        Uuid::parse_str(&session_id).map_err(|e| anyhow::anyhow!("Invalid session ID: {}", e))?;
110
111    let system = get_memory_system().await?;
112    let session_arc = system
113        .get_session(uuid)
114        .await
115        .map_err(|e| anyhow::anyhow!("Failed to load session: {}", e))?;
116
117    let session = session_arc.load();
118    let insights = SummaryGenerator::extract_key_insights(&session, limit.unwrap_or(5));
119
120    Ok(MCPToolResult::success(
121        format!("Found {} key insights", insights.len()),
122        Some(serde_json::to_value(insights)?),
123    ))
124}
125
126/// Analyse entity importance with optional minimum-score and limit filters.
127pub async fn get_entity_importance_analysis(
128    session_id: String,
129    limit: Option<usize>,
130    min_importance: Option<f32>,
131) -> Result<MCPToolResult> {
132    let uuid =
133        Uuid::parse_str(&session_id).map_err(|e| anyhow::anyhow!("Invalid session ID: {}", e))?;
134
135    let system = get_memory_system().await?;
136    let session_arc = system
137        .get_session(uuid)
138        .await
139        .map_err(|e| anyhow::anyhow!("Failed to load session: {}", e))?;
140
141    let session = session_arc.load();
142    let mut analysis = session.entity_graph.analyze_entity_importance();
143
144    let total_entities = analysis.len();
145
146    if let Some(min_imp) = min_importance {
147        analysis.retain(|entity| entity.importance_score >= min_imp);
148    }
149
150    let after_filter = analysis.len();
151
152    let entity_limit = limit.unwrap_or(100);
153    let truncated = analysis.len() > entity_limit;
154    if truncated {
155        analysis.truncate(entity_limit);
156    }
157
158    let result = serde_json::json!({
159        "entities": analysis,
160        "pagination": {
161            "total_entities": total_entities,
162            "after_filter": after_filter,
163            "shown": if truncated { entity_limit } else { after_filter },
164            "truncated": truncated
165        }
166    });
167
168    Ok(MCPToolResult::success(
169        format!(
170            "Analyzed {} entities (showing {})",
171            total_entities,
172            if truncated {
173                entity_limit
174            } else {
175                after_filter
176            }
177        ),
178        Some(result),
179    ))
180}
181
182/// Build a network view of entities and their relationships.
183pub async fn get_entity_network_view(
184    session_id: String,
185    center_entity: Option<String>,
186    max_entities: Option<usize>,
187    max_relationships: Option<usize>,
188) -> Result<MCPToolResult> {
189    let uuid =
190        Uuid::parse_str(&session_id).map_err(|e| anyhow::anyhow!("Invalid session ID: {}", e))?;
191
192    let system = get_memory_system().await?;
193    let session_arc = system
194        .get_session(uuid)
195        .await
196        .map_err(|e| anyhow::anyhow!("Failed to load session: {}", e))?;
197
198    let session = session_arc.load();
199
200    let max_entities = max_entities.unwrap_or(50);
201    let max_relationships = max_relationships.unwrap_or(100);
202
203    let entities: Vec<serde_json::Value> = session
204        .entity_graph
205        .get_most_important_entities(max_entities)
206        .into_iter()
207        .map(|e| {
208            serde_json::json!({
209                "name": e.name,
210                "entity_type": format!("{:?}", e.entity_type),
211                "importance_score": e.importance_score,
212                "mention_count": e.mention_count
213            })
214        })
215        .collect();
216
217    let relationships: Vec<serde_json::Value> = if let Some(center) = &center_entity {
218        session
219            .entity_graph
220            .trace_entity_relationships(center, 2)
221            .into_iter()
222            .take(max_relationships)
223            .map(|(entity, rel_type, target)| {
224                serde_json::json!({
225                    "from": entity,
226                    "type": format!("{:?}", rel_type),
227                    "to": target
228                })
229            })
230            .collect()
231    } else {
232        session
233            .entity_graph
234            .get_all_relationships()
235            .into_iter()
236            .take(max_relationships)
237            .map(|r| {
238                serde_json::json!({
239                    "from": r.from_entity,
240                    "type": format!("{:?}", r.relation_type),
241                    "to": r.to_entity
242                })
243            })
244            .collect()
245    };
246
247    Ok(MCPToolResult::success(
248        format!(
249            "Network view: {} entities, {} relationships",
250            entities.len(),
251            relationships.len()
252        ),
253        Some(serde_json::json!({
254            "session_id": session_id,
255            "center_entity": center_entity,
256            "entities": entities,
257            "relationships": relationships
258        })),
259    ))
260}
261
262/// Return detailed statistics about a session's size and activity.
263pub async fn get_session_statistics(session_id: String) -> Result<MCPToolResult> {
264    let uuid =
265        Uuid::parse_str(&session_id).map_err(|e| anyhow::anyhow!("Invalid session ID: {}", e))?;
266
267    let system = get_memory_system().await?;
268    let session_arc = system
269        .get_session(uuid)
270        .await
271        .map_err(|e| anyhow::anyhow!("Failed to load session: {}", e))?;
272
273    let session = session_arc.load();
274
275    let hot_updates = session.hot_context.len();
276    let warm_updates = session.warm_context.len();
277    let incremental_updates = session.incremental_updates.len();
278    let total_updates = hot_updates + warm_updates + incremental_updates;
279    let entity_count = session.entity_graph.entities.len();
280    let relationship_count = session.entity_graph.get_all_relationships().len();
281    let code_refs = session.code_references.len();
282    let change_history = session.change_history.len();
283
284    let first_update = session
285        .incremental_updates
286        .iter()
287        .min_by_key(|u| u.timestamp)
288        .map(|u| u.timestamp.to_rfc3339());
289    let last_update = session
290        .incremental_updates
291        .iter()
292        .max_by_key(|u| u.timestamp)
293        .map(|u| u.timestamp.to_rfc3339());
294
295    let duration = match (first_update.clone(), last_update.clone()) {
296        (Some(first), Some(last)) => {
297            let first_dt = chrono::DateTime::parse_from_rfc3339(&first).ok();
298            let last_dt = chrono::DateTime::parse_from_rfc3339(&last).ok();
299            match (first_dt, last_dt) {
300                (Some(f), Some(l)) => Some((l - f).num_minutes()),
301                _ => None,
302            }
303        }
304        _ => None,
305    };
306
307    Ok(MCPToolResult::success(
308        format!("Session statistics for {}", session_id),
309        Some(serde_json::json!({
310            "session_id": session_id,
311            "name": session.name(),
312            "description": session.description(),
313            "created_at": session.created_at().to_rfc3339(),
314            "last_updated": session.last_updated.to_rfc3339(),
315            "hot_updates": hot_updates,
316            "warm_updates": warm_updates,
317            "incremental_updates": incremental_updates,
318            "total_updates": total_updates,
319            "entity_count": entity_count,
320            "relationship_count": relationship_count,
321            "code_references": code_refs,
322            "change_history": change_history,
323            "first_update": first_update,
324            "last_update": last_update,
325            "duration_minutes": duration,
326            "vectorized_count": 0
327        })),
328    ))
329}
330
331/// Return a JSON catalog describing every available MCP tool and its usage.
332pub async fn get_tool_catalog() -> Result<MCPToolResult> {
333    let catalog = serde_json::json!({
334        "total_tools": 26,
335        "categories": {
336            "Session Management": {
337                "description": "Tools for creating, loading, and managing conversation sessions",
338                "tool_count": 5,
339                "tools": [
340                    {
341                        "name": "create_session",
342                        "description": "Start new conversation session with optional name/description",
343                        "use_when": "Beginning a new project, conversation, or knowledge context"
344                    },
345                    {
346                        "name": "load_session",
347                        "description": "Resume existing session into active memory",
348                        "use_when": "Continuing work on a previous conversation or project"
349                    },
350                    {
351                        "name": "list_sessions",
352                        "description": "View all sessions with metadata and statistics",
353                        "use_when": "Finding available sessions or checking session details"
354                    },
355                    {
356                        "name": "search_sessions",
357                        "description": "Find sessions by name or description (text search)",
358                        "use_when": "Looking for specific sessions when you know the name/topic"
359                    },
360                    {
361                        "name": "update_session_metadata",
362                        "description": "Change session name or description",
363                        "use_when": "Renaming or updating session information"
364                    }
365                ]
366            },
367            "Context Operations": {
368                "description": "Tools for adding and querying conversation knowledge",
369                "tool_count": 3,
370                "tools": [
371                    {
372                        "name": "update_conversation_context",
373                        "description": "Add knowledge: QA, decisions, problems solved, code changes",
374                        "use_when": "Storing information for future retrieval and learning",
375                        "interaction_types": ["qa", "decision_made", "problem_solved", "code_change", "requirement_added", "concept_defined"]
376                    },
377                    {
378                        "name": "query_conversation_context",
379                        "description": "Search using entities, keywords, or structured queries",
380                        "use_when": "Finding exact entities or keyword-based searches (faster than semantic)",
381                        "query_types": ["find_related_entities", "get_entity_context", "search_updates", "get_most_important_entities"]
382                    },
383                    {
384                        "name": "create_session_checkpoint",
385                        "description": "Create snapshot of current session state",
386                        "use_when": "Creating restore points before major changes"
387                    }
388                ]
389            },
390            "Semantic Search": {
391                "description": "AI-powered conceptual search using embeddings (requires embeddings feature)",
392                "tool_count": 5,
393                "tools": [
394                    {
395                        "name": "semantic_search_session",
396                        "description": "AI search within session - auto-loads and auto-vectorizes!",
397                        "use_when": "Finding information by concept/meaning, not exact keywords",
398                        "note": "Automatically vectorizes on first use - no manual setup needed"
399                    },
400                    {
401                        "name": "semantic_search_global",
402                        "description": "AI search across ALL sessions",
403                        "use_when": "Finding related knowledge across multiple conversations"
404                    },
405                    {
406                        "name": "find_related_content",
407                        "description": "Discover connections between different sessions",
408                        "use_when": "Finding similar discussions from other conversations"
409                    },
410                    {
411                        "name": "vectorize_session",
412                        "description": "Generate embeddings for semantic search (optional - auto-called)",
413                        "use_when": "Rarely needed - semantic_search_session auto-vectorizes",
414                        "note": "Only useful for batch processing or manual control"
415                    },
416                    {
417                        "name": "get_vectorization_stats",
418                        "description": "Check embedding statistics and performance metrics",
419                        "use_when": "Debugging or monitoring semantic search system"
420                    }
421                ]
422            },
423            "Analysis & Insights": {
424                "description": "Tools for extracting insights, summaries, and analyzing session data",
425                "tool_count": 7,
426                "tools": [
427                    {
428                        "name": "get_structured_summary",
429                        "description": "Comprehensive summary with auto-compact for large sessions",
430                        "use_when": "Getting overview of decisions, entities, questions, concepts",
431                        "note": "Auto-compacts if response > 25K tokens - prevents MCP overflow"
432                    },
433                    {
434                        "name": "get_key_decisions",
435                        "description": "Timeline of decisions with confidence levels",
436                        "use_when": "Reviewing architectural choices and their rationale"
437                    },
438                    {
439                        "name": "get_key_insights",
440                        "description": "Extract top insights from session data",
441                        "use_when": "Quick overview of most important discoveries"
442                    },
443                    {
444                        "name": "get_entity_importance_analysis",
445                        "description": "Entity rankings with mention counts and relationships",
446                        "use_when": "Understanding which concepts/technologies are most central"
447                    },
448                    {
449                        "name": "get_entity_network_view",
450                        "description": "Visualize entity relationships as network graph",
451                        "use_when": "Understanding how concepts connect to each other"
452                    },
453                    {
454                        "name": "get_session_statistics",
455                        "description": "Session metrics: updates, entities, activity level, duration",
456                        "use_when": "Checking session health and growth metrics"
457                    },
458                    {
459                        "name": "get_tool_catalog",
460                        "description": "View all 26 tools organized by category with usage guidance",
461                        "use_when": "First time using post-cortex or discovering available tools",
462                        "note": "Returns categories, workflows, tips, and most-used tools list"
463                    }
464                ]
465            },
466            "Workspace Management": {
467                "description": "Tools for organizing related sessions (e.g., microservices, monorepo projects)",
468                "tool_count": 6,
469                "tools": [
470                    {
471                        "name": "create_workspace",
472                        "description": "Create workspace to group related sessions",
473                        "use_when": "Starting a multi-service project or organizing session groups"
474                    },
475                    {
476                        "name": "get_workspace",
477                        "description": "Retrieve workspace details including all sessions and metadata",
478                        "use_when": "Viewing workspace configuration and session associations"
479                    },
480                    {
481                        "name": "list_workspaces",
482                        "description": "List all workspaces with session counts",
483                        "use_when": "Finding available workspaces or checking workspace overview"
484                    },
485                    {
486                        "name": "delete_workspace",
487                        "description": "Delete workspace (sessions remain intact)",
488                        "use_when": "Removing workspace organization without deleting sessions"
489                    },
490                    {
491                        "name": "add_session_to_workspace",
492                        "description": "Add session to workspace with role (primary/related/dependency/shared)",
493                        "use_when": "Organizing session into workspace with specific relationship role"
494                    },
495                    {
496                        "name": "remove_session_from_workspace",
497                        "description": "Remove session from workspace",
498                        "use_when": "Reorganizing sessions or removing from workspace group"
499                    }
500                ]
501            }
502        },
503        "getting_started": [
504            "1. create_session → get session_id",
505            "2. update_conversation_context → add knowledge (qa, decisions, problems, code)",
506            "3. semantic_search_session → AI-powered search (auto-vectorizes on first use!)",
507            "4. get_structured_summary → comprehensive overview"
508        ],
509        "most_used_tools": [
510            "create_session",
511            "load_session",
512            "update_conversation_context",
513            "semantic_search_session",
514            "get_structured_summary"
515        ],
516        "tips": [
517            "Semantic search auto-vectorizes - no manual vectorize_session needed!",
518            "Use semantic_search for concepts, query_conversation_context for keywords",
519            "get_structured_summary has auto-compact - safe for large sessions",
520            "Similarity scores: 0.65-0.75 = excellent, 0.45-0.55 = good, <0.30 = weak",
521            "All sessions persist across Claude Code conversations"
522        ]
523    });
524
525    Ok(MCPToolResult::success(
526        "Retrieved tool catalog with 26 tools across 5 categories".to_string(),
527        Some(catalog),
528    ))
529}