feat: Add Memory system with Weaviate integration and MCP tools

MEMORY SYSTEM ARCHITECTURE:
- Weaviate-based memory storage (Thought, Message, Conversation collections)
- GPU embeddings with BAAI/bge-m3 (1024-dim, RTX 4070)
- 9 MCP tools for Claude Desktop integration

CORE MODULES (memory/):
- core/embedding_service.py: GPU embedder singleton with PyTorch
- schemas/memory_schemas.py: Weaviate schema definitions
- mcp/thought_tools.py: add_thought, search_thoughts, get_thought
- mcp/message_tools.py: add_message, get_messages, search_messages
- mcp/conversation_tools.py: get_conversation, search_conversations, list_conversations

FLASK TEMPLATES:
- conversation_view.html: Display single conversation with messages
- conversations.html: List all conversations with search
- memories.html: Browse and search thoughts

FEATURES:
- Semantic search across thoughts, messages, conversations
- Privacy levels (private, shared, public)
- Thought types (reflection, question, intuition, observation)
- Conversation categories with filtering
- Message ordering and role-based display

DATA (as of 2026-01-08):
- 102 Thoughts
- 377 Messages
- 12 Conversations

DOCUMENTATION:
- memory/README_MCP_TOOLS.md: Complete API reference and usage examples

All MCP tools tested and validated (see test_memory_mcp_tools.py in archive).

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-08 18:08:13 +01:00
parent 187ba4854e
commit 2f34125ef6
13 changed files with 2145 additions and 0 deletions

View File

@@ -0,0 +1,77 @@
{% extends "base.html" %}
{% block title %}Conversation: {{ conversation.conversation_id }}{% endblock %}
{% block content %}
<section class="section">
<h1 class="text-center">{{ conversation.conversation_id }}</h1>
<p class="lead text-center">Conversation David-Ikario</p>
<div class="ornament">· · ·</div>
<!-- Conversation Metadata -->
<div class="card">
<h2>📝 Détails de la conversation</h2>
<div class="mt-2">
<p><strong>Catégorie:</strong> <span class="badge">{{ conversation.category }}</span></p>
<p><strong>Participants:</strong> {{ conversation.participants|join(', ') if conversation.participants else 'N/A' }}</p>
<p><strong>Nombre de messages:</strong> {{ conversation.message_count }}</p>
<p><strong>Date de début:</strong> {{ conversation.timestamp_start[:19].replace('T', ' ') if conversation.timestamp_start else 'Inconnue' }}</p>
{% if conversation.timestamp_end %}
<p><strong>Date de fin:</strong> {{ conversation.timestamp_end[:19].replace('T', ' ') }}</p>
{% endif %}
{% if conversation.tags and conversation.tags|length > 0 %}
<p><strong>Tags:</strong> {{ conversation.tags|join(', ') }}</p>
{% endif %}
</div>
{% if conversation.summary %}
<hr style="margin: 1rem 0; border: none; border-top: 1px solid var(--color-bg-secondary);">
<div>
<strong>Résumé:</strong>
<p style="margin-top: 0.5rem; color: var(--color-text-main);">{{ conversation.summary }}</p>
</div>
{% endif %}
</div>
<hr class="divider">
<!-- Messages -->
<div class="card">
<h2>💬 Messages ({{ messages|length }})</h2>
{% if messages %}
<div class="mt-2">
{% for msg in messages %}
<div class="message-item" style="margin-bottom: 1.5rem; padding: 1rem; background: {% if msg.role == 'user' %}var(--color-bg-secondary){% else %}#fff{% endif %}; border-radius: 8px; border-left: 4px solid {% if msg.role == 'user' %}var(--color-accent){% else %}var(--color-accent-alt){% endif %};">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem;">
<span class="badge">{{ msg.role }}</span>
<span class="text-muted" style="font-size: 0.875rem;">
{{ msg.timestamp[:19].replace('T', ' ') if msg.timestamp else 'Date inconnue' }}
| Index: {{ msg.order_index }}
</span>
</div>
<div style="line-height: 1.6; white-space: pre-wrap;">{{ msg.content }}</div>
</div>
{% endfor %}
</div>
{% else %}
<p class="text-muted mt-2">Aucun message dans cette conversation</p>
{% endif %}
</div>
<div class="text-center mt-3">
<a href="/conversations" class="btn">← Retour aux conversations</a>
</div>
</section>
<style>
.message-item {
transition: transform 0.2s ease;
}
.message-item:hover {
transform: translateX(4px);
}
</style>
{% endblock %}

View File

@@ -0,0 +1,124 @@
{% extends "base.html" %}
{% block title %}Conversations{% endblock %}
{% block content %}
<section class="section">
<h1 class="text-center">Conversations Ikario</h1>
<p class="lead text-center">Liste des conversations David-Ikario</p>
<div class="ornament">· · ·</div>
<!-- Search Box -->
<div class="card">
<h2>🔍 Recherche sémantique</h2>
<div class="form-row">
<input
type="text"
id="searchQuery"
class="form-control"
placeholder="Ex: discussions sur la philosophie..."
style="flex: 1;"
>
<button onclick="searchConversations()" class="btn btn-primary">Rechercher</button>
</div>
<div id="searchResults" class="mt-3"></div>
</div>
<hr class="divider">
<!-- Conversations List -->
<div class="card">
<h2>📚 Toutes les conversations ({{ conversations|length }})</h2>
{% if conversations %}
<div class="results-list mt-2">
{% for conv in conversations %}
<div class="result-item">
<div class="result-header">
<a href="/conversation/{{ conv.conversation_id }}" style="font-weight: 600;">
{{ conv.conversation_id }}
</a>
<span class="badge">{{ conv.category }}</span>
<span class="text-muted">{{ conv.message_count }} messages</span>
</div>
<div class="result-text">{{ conv.summary }}</div>
<div class="result-meta">
<span class="text-muted">
{{ conv.timestamp_start[:10] if conv.timestamp_start else 'Date inconnue' }}
</span>
<span class="text-muted">
Participants: {{ conv.participants|join(', ') if conv.participants else 'N/A' }}
</span>
</div>
</div>
{% endfor %}
</div>
{% else %}
<p class="text-muted mt-2">Aucune conversation trouvée</p>
{% endif %}
</div>
<div class="text-center mt-3">
<a href="/memories" class="btn">Retour aux recherches Memory</a>
</div>
</section>
<script>
async function searchConversations() {
const query = document.getElementById('searchQuery').value;
const resultsDiv = document.getElementById('searchResults');
if (!query.trim()) {
resultsDiv.innerHTML = '<p class="text-muted">Entrez une requête de recherche</p>';
return;
}
resultsDiv.innerHTML = '<p class="text-muted">Recherche en cours...</p>';
try {
const response = await fetch('/api/conversations/search', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({query, limit: 10})
});
const data = await response.json();
if (data.success && data.results && data.results.length > 0) {
let html = '<div class="results-list">';
data.results.forEach((conv, idx) => {
html += `
<div class="result-item">
<div class="result-header">
<a href="/conversation/${conv.conversation_id}" style="font-weight: 600;">
${conv.conversation_id}
</a>
<span class="badge">${conv.category}</span>
<span class="text-muted">${conv.message_count} messages</span>
</div>
<div class="result-text">${conv.summary}</div>
<div class="result-meta">
<span class="text-muted">
${conv.timestamp_start ? conv.timestamp_start.substring(0,10) : 'Date inconnue'}
</span>
</div>
</div>
`;
});
html += '</div>';
resultsDiv.innerHTML = html;
} else {
resultsDiv.innerHTML = '<p class="text-muted">Aucun résultat trouvé</p>';
}
} catch (error) {
resultsDiv.innerHTML = `<p class="text-danger">Erreur: ${error.message}</p>';
}
}
// Allow Enter key to trigger search
document.getElementById('searchQuery').addEventListener('keypress', function(e) {
if (e.key === 'Enter') searchConversations();
});
</script>
{% endblock %}

View File

@@ -0,0 +1,172 @@
{% extends "base.html" %}
{% block title %}Memory Thoughts & Messages{% endblock %}
{% block content %}
<section class="section">
<h1 class="text-center">Memory System</h1>
<p class="lead text-center">Recherche sémantique dans les pensées et messages d'Ikario</p>
<div class="ornament">· · ·</div>
<!-- Statistics -->
<div class="stats-grid">
<div class="stat-box">
<div class="stat-number">{{ stats.thoughts }}</div>
<div class="stat-label">Pensées</div>
</div>
<div class="stat-box">
<div class="stat-number">{{ stats.messages }}</div>
<div class="stat-label">Messages</div>
</div>
<div class="stat-box">
<div class="stat-number">{{ stats.conversations }}</div>
<div class="stat-label">Conversations</div>
</div>
</div>
<hr class="divider">
<!-- Search Thoughts -->
<div class="card">
<h2>🧠 Rechercher dans les pensées</h2>
<div class="form-row">
<input
type="text"
id="thoughtQuery"
class="form-control"
placeholder="Ex: réflexions sur la conscience..."
style="flex: 1;"
>
<button onclick="searchThoughts()" class="btn btn-primary">Rechercher</button>
</div>
<div id="thoughtResults" class="mt-3"></div>
</div>
<hr class="divider">
<!-- Search Messages -->
<div class="card">
<h2>💬 Rechercher dans les messages</h2>
<div class="form-row">
<input
type="text"
id="messageQuery"
class="form-control"
placeholder="Ex: discussions sur l'intelligence artificielle..."
style="flex: 1;"
>
<button onclick="searchMessages()" class="btn btn-primary">Rechercher</button>
</div>
<div id="messageResults" class="mt-3"></div>
</div>
<hr class="divider">
<div class="text-center">
<a href="/conversations" class="btn">Voir toutes les conversations</a>
</div>
</section>
<script>
async function searchThoughts() {
const query = document.getElementById('thoughtQuery').value;
const resultsDiv = document.getElementById('thoughtResults');
if (!query.trim()) {
resultsDiv.innerHTML = '<p class="text-muted">Entrez une requête de recherche</p>';
return;
}
resultsDiv.innerHTML = '<p class="text-muted">Recherche en cours...</p>';
try {
const response = await fetch('/api/memories/search-thoughts', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({query, limit: 10})
});
const data = await response.json();
if (data.success && data.results && data.results.length > 0) {
let html = '<div class="results-list">';
data.results.forEach((thought, idx) => {
html += `
<div class="result-item">
<div class="result-header">
<span class="badge">${thought.thought_type}</span>
<span class="text-muted">${new Date(thought.timestamp).toLocaleDateString('fr-FR')}</span>
</div>
<div class="result-text">${thought.content}</div>
${thought.concepts && thought.concepts.length > 0 ?
`<div class="mt-1">
<strong>Concepts:</strong> ${thought.concepts.join(', ')}
</div>` : ''}
</div>
`;
});
html += '</div>';
resultsDiv.innerHTML = html;
} else {
resultsDiv.innerHTML = '<p class="text-muted">Aucun résultat trouvé</p>';
}
} catch (error) {
resultsDiv.innerHTML = `<p class="text-danger">Erreur: ${error.message}</p>`;
}
}
async function searchMessages() {
const query = document.getElementById('messageQuery').value;
const resultsDiv = document.getElementById('messageResults');
if (!query.trim()) {
resultsDiv.innerHTML = '<p class="text-muted">Entrez une requête de recherche</p>';
return;
}
resultsDiv.innerHTML = '<p class="text-muted">Recherche en cours...</p>';
try {
const response = await fetch('/api/memories/search-messages', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({query, limit: 10})
});
const data = await response.json();
if (data.success && data.results && data.results.length > 0) {
let html = '<div class="results-list">';
data.results.forEach((msg, idx) => {
html += `
<div class="result-item">
<div class="result-header">
<span class="badge">${msg.role}</span>
<span class="text-muted">${new Date(msg.timestamp).toLocaleDateString('fr-FR')}</span>
<span class="text-muted">Conv: ${msg.conversation_id}</span>
</div>
<div class="result-text">${msg.content.substring(0, 300)}${msg.content.length > 300 ? '...' : ''}</div>
</div>
`;
});
html += '</div>';
resultsDiv.innerHTML = html;
} else {
resultsDiv.innerHTML = '<p class="text-muted">Aucun résultat trouvé</p>';
}
} catch (error) {
resultsDiv.innerHTML = `<p class="text-danger">Erreur: ${error.message}</p>`;
}
}
// Allow Enter key to trigger search
document.getElementById('thoughtQuery').addEventListener('keypress', function(e) {
if (e.key === 'Enter') searchThoughts();
});
document.getElementById('messageQuery').addEventListener('keypress', function(e) {
if (e.key === 'Enter') searchMessages();
});
</script>
{% endblock %}