Skip to main content

post_cortex_mcp/
analysis.rs

1//! Analysis, summaries, insights, and session statistics.
2
3use post_cortex_core::summary::SummaryGenerator;
4use crate::{get_memory_system, MCPToolResult};
5use anyhow::Result;
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!("Analyzed {} entities (showing {})", total_entities, if truncated { entity_limit } else { after_filter }),
170        Some(result),
171    ))
172}
173
174/// Build a network view of entities and their relationships.
175pub async fn get_entity_network_view(
176    session_id: String,
177    center_entity: Option<String>,
178    max_entities: Option<usize>,
179    max_relationships: Option<usize>,
180) -> Result<MCPToolResult> {
181    let uuid =
182        Uuid::parse_str(&session_id).map_err(|e| anyhow::anyhow!("Invalid session ID: {}", e))?;
183
184    let system = get_memory_system().await?;
185    let session_arc = system
186        .get_session(uuid)
187        .await
188        .map_err(|e| anyhow::anyhow!("Failed to load session: {}", e))?;
189
190    let session = session_arc.load();
191
192    let max_entities = max_entities.unwrap_or(50);
193    let max_relationships = max_relationships.unwrap_or(100);
194
195    let entities: Vec<serde_json::Value> = session
196        .entity_graph
197        .get_most_important_entities(max_entities)
198        .into_iter()
199        .map(|e| {
200            serde_json::json!({
201                "name": e.name,
202                "entity_type": format!("{:?}", e.entity_type),
203                "importance_score": e.importance_score,
204                "mention_count": e.mention_count
205            })
206        })
207        .collect();
208
209    let relationships: Vec<serde_json::Value> = if let Some(center) = &center_entity {
210        session
211            .entity_graph
212            .trace_entity_relationships(center, 2)
213            .into_iter()
214            .take(max_relationships)
215            .map(|(entity, rel_type, target)| {
216                serde_json::json!({
217                    "from": entity,
218                    "type": format!("{:?}", rel_type),
219                    "to": target
220                })
221            })
222            .collect()
223    } else {
224        session
225            .entity_graph
226            .get_all_relationships()
227            .into_iter()
228            .take(max_relationships)
229            .map(|r| {
230                serde_json::json!({
231                    "from": r.from_entity,
232                    "type": format!("{:?}", r.relation_type),
233                    "to": r.to_entity
234                })
235            })
236            .collect()
237    };
238
239    Ok(MCPToolResult::success(
240        format!(
241            "Network view: {} entities, {} relationships",
242            entities.len(),
243            relationships.len()
244        ),
245        Some(serde_json::json!({
246            "session_id": session_id,
247            "center_entity": center_entity,
248            "entities": entities,
249            "relationships": relationships
250        })),
251    ))
252}
253
254/// Return detailed statistics about a session's size and activity.
255pub async fn get_session_statistics(session_id: String) -> Result<MCPToolResult> {
256    let uuid =
257        Uuid::parse_str(&session_id).map_err(|e| anyhow::anyhow!("Invalid session ID: {}", e))?;
258
259    let system = get_memory_system().await?;
260    let session_arc = system
261        .get_session(uuid)
262        .await
263        .map_err(|e| anyhow::anyhow!("Failed to load session: {}", e))?;
264
265    let session = session_arc.load();
266
267    let hot_updates = session.hot_context.len();
268    let warm_updates = session.warm_context.len();
269    let incremental_updates = session.incremental_updates.len();
270    let total_updates = hot_updates + warm_updates + incremental_updates;
271    let entity_count = session.entity_graph.entities.len();
272    let relationship_count = session.entity_graph.get_all_relationships().len();
273    let code_refs = session.code_references.len();
274    let change_history = session.change_history.len();
275
276    let first_update = session
277        .incremental_updates
278        .iter()
279        .min_by_key(|u| u.timestamp)
280        .map(|u| u.timestamp.to_rfc3339());
281    let last_update = session
282        .incremental_updates
283        .iter()
284        .max_by_key(|u| u.timestamp)
285        .map(|u| u.timestamp.to_rfc3339());
286
287    let duration = match (first_update.clone(), last_update.clone()) {
288        (Some(first), Some(last)) => {
289            let first_dt = chrono::DateTime::parse_from_rfc3339(&first).ok();
290            let last_dt = chrono::DateTime::parse_from_rfc3339(&last).ok();
291            match (first_dt, last_dt) {
292                (Some(f), Some(l)) => Some((l - f).num_minutes()),
293                _ => None,
294            }
295        }
296        _ => None,
297    };
298
299    Ok(MCPToolResult::success(
300        format!("Session statistics for {}", session_id),
301        Some(serde_json::json!({
302            "session_id": session_id,
303            "name": session.name(),
304            "description": session.description(),
305            "created_at": session.created_at().to_rfc3339(),
306            "last_updated": session.last_updated.to_rfc3339(),
307            "hot_updates": hot_updates,
308            "warm_updates": warm_updates,
309            "incremental_updates": incremental_updates,
310            "total_updates": total_updates,
311            "entity_count": entity_count,
312            "relationship_count": relationship_count,
313            "code_references": code_refs,
314            "change_history": change_history,
315            "first_update": first_update,
316            "last_update": last_update,
317            "duration_minutes": duration,
318            "vectorized_count": 0
319        })),
320    ))
321}
322
323/// Return a JSON catalog describing every available MCP tool and its usage.
324pub async fn get_tool_catalog() -> Result<MCPToolResult> {
325    let catalog = serde_json::json!({
326        "total_tools": 26,
327        "categories": {
328            "Session Management": {
329                "description": "Tools for creating, loading, and managing conversation sessions",
330                "tool_count": 5,
331                "tools": [
332                    {
333                        "name": "create_session",
334                        "description": "Start new conversation session with optional name/description",
335                        "use_when": "Beginning a new project, conversation, or knowledge context"
336                    },
337                    {
338                        "name": "load_session",
339                        "description": "Resume existing session into active memory",
340                        "use_when": "Continuing work on a previous conversation or project"
341                    },
342                    {
343                        "name": "list_sessions",
344                        "description": "View all sessions with metadata and statistics",
345                        "use_when": "Finding available sessions or checking session details"
346                    },
347                    {
348                        "name": "search_sessions",
349                        "description": "Find sessions by name or description (text search)",
350                        "use_when": "Looking for specific sessions when you know the name/topic"
351                    },
352                    {
353                        "name": "update_session_metadata",
354                        "description": "Change session name or description",
355                        "use_when": "Renaming or updating session information"
356                    }
357                ]
358            },
359            "Context Operations": {
360                "description": "Tools for adding and querying conversation knowledge",
361                "tool_count": 3,
362                "tools": [
363                    {
364                        "name": "update_conversation_context",
365                        "description": "Add knowledge: QA, decisions, problems solved, code changes",
366                        "use_when": "Storing information for future retrieval and learning",
367                        "interaction_types": ["qa", "decision_made", "problem_solved", "code_change", "requirement_added", "concept_defined"]
368                    },
369                    {
370                        "name": "query_conversation_context",
371                        "description": "Search using entities, keywords, or structured queries",
372                        "use_when": "Finding exact entities or keyword-based searches (faster than semantic)",
373                        "query_types": ["find_related_entities", "get_entity_context", "search_updates", "get_most_important_entities"]
374                    },
375                    {
376                        "name": "create_session_checkpoint",
377                        "description": "Create snapshot of current session state",
378                        "use_when": "Creating restore points before major changes"
379                    }
380                ]
381            },
382            "Semantic Search": {
383                "description": "AI-powered conceptual search using embeddings (requires embeddings feature)",
384                "tool_count": 5,
385                "tools": [
386                    {
387                        "name": "semantic_search_session",
388                        "description": "AI search within session - auto-loads and auto-vectorizes!",
389                        "use_when": "Finding information by concept/meaning, not exact keywords",
390                        "note": "Automatically vectorizes on first use - no manual setup needed"
391                    },
392                    {
393                        "name": "semantic_search_global",
394                        "description": "AI search across ALL sessions",
395                        "use_when": "Finding related knowledge across multiple conversations"
396                    },
397                    {
398                        "name": "find_related_content",
399                        "description": "Discover connections between different sessions",
400                        "use_when": "Finding similar discussions from other conversations"
401                    },
402                    {
403                        "name": "vectorize_session",
404                        "description": "Generate embeddings for semantic search (optional - auto-called)",
405                        "use_when": "Rarely needed - semantic_search_session auto-vectorizes",
406                        "note": "Only useful for batch processing or manual control"
407                    },
408                    {
409                        "name": "get_vectorization_stats",
410                        "description": "Check embedding statistics and performance metrics",
411                        "use_when": "Debugging or monitoring semantic search system"
412                    }
413                ]
414            },
415            "Analysis & Insights": {
416                "description": "Tools for extracting insights, summaries, and analyzing session data",
417                "tool_count": 7,
418                "tools": [
419                    {
420                        "name": "get_structured_summary",
421                        "description": "Comprehensive summary with auto-compact for large sessions",
422                        "use_when": "Getting overview of decisions, entities, questions, concepts",
423                        "note": "Auto-compacts if response > 25K tokens - prevents MCP overflow"
424                    },
425                    {
426                        "name": "get_key_decisions",
427                        "description": "Timeline of decisions with confidence levels",
428                        "use_when": "Reviewing architectural choices and their rationale"
429                    },
430                    {
431                        "name": "get_key_insights",
432                        "description": "Extract top insights from session data",
433                        "use_when": "Quick overview of most important discoveries"
434                    },
435                    {
436                        "name": "get_entity_importance_analysis",
437                        "description": "Entity rankings with mention counts and relationships",
438                        "use_when": "Understanding which concepts/technologies are most central"
439                    },
440                    {
441                        "name": "get_entity_network_view",
442                        "description": "Visualize entity relationships as network graph",
443                        "use_when": "Understanding how concepts connect to each other"
444                    },
445                    {
446                        "name": "get_session_statistics",
447                        "description": "Session metrics: updates, entities, activity level, duration",
448                        "use_when": "Checking session health and growth metrics"
449                    },
450                    {
451                        "name": "get_tool_catalog",
452                        "description": "View all 26 tools organized by category with usage guidance",
453                        "use_when": "First time using post-cortex or discovering available tools",
454                        "note": "Returns categories, workflows, tips, and most-used tools list"
455                    }
456                ]
457            },
458            "Workspace Management": {
459                "description": "Tools for organizing related sessions (e.g., microservices, monorepo projects)",
460                "tool_count": 6,
461                "tools": [
462                    {
463                        "name": "create_workspace",
464                        "description": "Create workspace to group related sessions",
465                        "use_when": "Starting a multi-service project or organizing session groups"
466                    },
467                    {
468                        "name": "get_workspace",
469                        "description": "Retrieve workspace details including all sessions and metadata",
470                        "use_when": "Viewing workspace configuration and session associations"
471                    },
472                    {
473                        "name": "list_workspaces",
474                        "description": "List all workspaces with session counts",
475                        "use_when": "Finding available workspaces or checking workspace overview"
476                    },
477                    {
478                        "name": "delete_workspace",
479                        "description": "Delete workspace (sessions remain intact)",
480                        "use_when": "Removing workspace organization without deleting sessions"
481                    },
482                    {
483                        "name": "add_session_to_workspace",
484                        "description": "Add session to workspace with role (primary/related/dependency/shared)",
485                        "use_when": "Organizing session into workspace with specific relationship role"
486                    },
487                    {
488                        "name": "remove_session_from_workspace",
489                        "description": "Remove session from workspace",
490                        "use_when": "Reorganizing sessions or removing from workspace group"
491                    }
492                ]
493            }
494        },
495        "getting_started": [
496            "1. create_session → get session_id",
497            "2. update_conversation_context → add knowledge (qa, decisions, problems, code)",
498            "3. semantic_search_session → AI-powered search (auto-vectorizes on first use!)",
499            "4. get_structured_summary → comprehensive overview"
500        ],
501        "most_used_tools": [
502            "create_session",
503            "load_session",
504            "update_conversation_context",
505            "semantic_search_session",
506            "get_structured_summary"
507        ],
508        "tips": [
509            "Semantic search auto-vectorizes - no manual vectorize_session needed!",
510            "Use semantic_search for concepts, query_conversation_context for keywords",
511            "get_structured_summary has auto-compact - safe for large sessions",
512            "Similarity scores: 0.65-0.75 = excellent, 0.45-0.55 = good, <0.30 = weak",
513            "All sessions persist across Claude Code conversations"
514        ]
515    });
516
517    Ok(MCPToolResult::success(
518        "Retrieved tool catalog with 26 tools across 5 categories".to_string(),
519        Some(catalog),
520    ))
521}