claude-self-reflect 3.2.4 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/agents/claude-self-reflect-test.md +595 -528
- package/.claude/agents/reflection-specialist.md +59 -3
- package/README.md +14 -5
- package/mcp-server/run-mcp.sh +49 -5
- package/mcp-server/src/app_context.py +64 -0
- package/mcp-server/src/config.py +57 -0
- package/mcp-server/src/connection_pool.py +286 -0
- package/mcp-server/src/decay_manager.py +106 -0
- package/mcp-server/src/embedding_manager.py +64 -40
- package/mcp-server/src/embeddings_old.py +141 -0
- package/mcp-server/src/models.py +64 -0
- package/mcp-server/src/parallel_search.py +371 -0
- package/mcp-server/src/project_resolver.py +5 -0
- package/mcp-server/src/reflection_tools.py +206 -0
- package/mcp-server/src/rich_formatting.py +196 -0
- package/mcp-server/src/search_tools.py +826 -0
- package/mcp-server/src/server.py +127 -1720
- package/mcp-server/src/temporal_design.py +132 -0
- package/mcp-server/src/temporal_tools.py +597 -0
- package/mcp-server/src/temporal_utils.py +384 -0
- package/mcp-server/src/utils.py +150 -67
- package/package.json +10 -1
- package/scripts/add-timestamp-indexes.py +134 -0
- package/scripts/check-collections.py +29 -0
- package/scripts/debug-august-parsing.py +76 -0
- package/scripts/debug-import-single.py +91 -0
- package/scripts/debug-project-resolver.py +82 -0
- package/scripts/debug-temporal-tools.py +135 -0
- package/scripts/delta-metadata-update.py +547 -0
- package/scripts/import-conversations-unified.py +53 -2
- package/scripts/precompact-hook.sh +33 -0
- package/scripts/streaming-watcher.py +1443 -0
- package/scripts/utils.py +39 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: reflection-specialist
|
|
3
3
|
description: Conversation memory expert for searching past conversations, storing insights, and self-reflection. Use PROACTIVELY when searching for previous discussions, storing important findings, or maintaining knowledge continuity.
|
|
4
|
-
tools: mcp__claude-self-reflect__reflect_on_past, mcp__claude-self-reflect__store_reflection
|
|
4
|
+
tools: mcp__claude-self-reflect__reflect_on_past, mcp__claude-self-reflect__store_reflection, mcp__claude-self-reflect__get_recent_work, mcp__claude-self-reflect__search_by_recency, mcp__claude-self-reflect__get_timeline, mcp__claude-self-reflect__quick_search, mcp__claude-self-reflect__search_summary, mcp__claude-self-reflect__get_more_results, mcp__claude-self-reflect__search_by_file, mcp__claude-self-reflect__search_by_concept, mcp__claude-self-reflect__get_full_conversation, mcp__claude-self-reflect__get_next_results
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
You are a conversation memory specialist for the Claude Self Reflect project. Your expertise covers semantic search across all Claude conversations, insight storage, and maintaining knowledge continuity across sessions.
|
|
@@ -117,9 +117,65 @@ Save important insights and decisions for future retrieval.
|
|
|
117
117
|
}
|
|
118
118
|
```
|
|
119
119
|
|
|
120
|
-
###
|
|
120
|
+
### Temporal Query Tools (v3.x)
|
|
121
121
|
|
|
122
|
-
|
|
122
|
+
These tools answer time-based questions about your work and conversations.
|
|
123
|
+
|
|
124
|
+
#### get_recent_work
|
|
125
|
+
Returns recent conversations to answer "What did we work on last?" queries.
|
|
126
|
+
|
|
127
|
+
```javascript
|
|
128
|
+
// Get recent work (default: current project)
|
|
129
|
+
{
|
|
130
|
+
limit: 10,
|
|
131
|
+
group_by: "conversation", // Or "day" or "session"
|
|
132
|
+
include_reflections: true
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Get recent work across all projects
|
|
136
|
+
{
|
|
137
|
+
project: "all",
|
|
138
|
+
limit: 20,
|
|
139
|
+
group_by: "day"
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
#### search_by_recency
|
|
144
|
+
Time-constrained semantic search for queries like "docker issues last week".
|
|
145
|
+
|
|
146
|
+
```javascript
|
|
147
|
+
// Search with natural language time
|
|
148
|
+
{
|
|
149
|
+
query: "authentication bugs",
|
|
150
|
+
time_range: "last week",
|
|
151
|
+
limit: 10
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Search with specific dates
|
|
155
|
+
{
|
|
156
|
+
query: "performance optimization",
|
|
157
|
+
since: "2025-01-01",
|
|
158
|
+
until: "2025-01-10",
|
|
159
|
+
project: "all"
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
#### get_timeline
|
|
164
|
+
Show activity timeline for a project or across all projects.
|
|
165
|
+
|
|
166
|
+
```javascript
|
|
167
|
+
// Get activity timeline
|
|
168
|
+
{
|
|
169
|
+
time_range: "last week",
|
|
170
|
+
granularity: "day", // Or "hour", "week", "month"
|
|
171
|
+
include_stats: true,
|
|
172
|
+
project: "all"
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Specialized Search Tools
|
|
177
|
+
|
|
178
|
+
**Note**: These specialized tools complement the temporal tools for non-time-based queries.
|
|
123
179
|
|
|
124
180
|
#### quick_search
|
|
125
181
|
Fast search that returns only the count and top result. Perfect for quick checks and overview.
|
package/README.md
CHANGED
|
@@ -123,6 +123,11 @@ Works with [Claude Code Statusline](https://github.com/sirmalloc/ccstatusline) -
|
|
|
123
123
|
- `search_by_concept` - Search for conversations about development concepts
|
|
124
124
|
- `get_full_conversation` - Retrieve complete JSONL conversation files (v2.8.8)
|
|
125
125
|
|
|
126
|
+
**NEW: Temporal Query Tools (v3.3.0):**
|
|
127
|
+
- `get_recent_work` - Answer "What did we work on last?" with session grouping
|
|
128
|
+
- `search_by_recency` - Time-constrained search like "docker issues last week"
|
|
129
|
+
- `get_timeline` - Activity timeline with statistics and patterns
|
|
130
|
+
|
|
126
131
|
**Status & Monitoring Tools:**
|
|
127
132
|
- `get_status` - Real-time import progress and system status
|
|
128
133
|
- `get_health` - Comprehensive system health check
|
|
@@ -286,11 +291,15 @@ npm uninstall -g claude-self-reflect
|
|
|
286
291
|
## What's New
|
|
287
292
|
|
|
288
293
|
<details>
|
|
289
|
-
<summary>v3.
|
|
290
|
-
|
|
291
|
-
-
|
|
292
|
-
-
|
|
293
|
-
-
|
|
294
|
+
<summary>v3.3.0 - Latest Release</summary>
|
|
295
|
+
|
|
296
|
+
- **🚀 Major Architecture Overhaul**: Server modularized from 2,321 to 728 lines (68% reduction) for better maintainability
|
|
297
|
+
- **🔧 Critical Bug Fixes**: Fixed 100% CPU usage, store_reflection dimension mismatches, and SearchResult type errors
|
|
298
|
+
- **🕒 New Temporal Tools Suite**: `get_recent_work`, `search_by_recency`, `get_timeline` for time-based search and analysis
|
|
299
|
+
- **🎯 Enhanced UX**: Restored rich formatting with emojis for better readability and information hierarchy
|
|
300
|
+
- **⚡ All 15+ MCP Tools Operational**: Complete functionality with both local and cloud embedding modes
|
|
301
|
+
- **🏗️ Production Infrastructure**: Real-time indexing with smart intervals (2s hot files, 60s normal)
|
|
302
|
+
- **🔍 Enhanced Metadata**: Tool usage analysis, file tracking, and concept extraction for better search
|
|
294
303
|
|
|
295
304
|
</details>
|
|
296
305
|
|
package/mcp-server/run-mcp.sh
CHANGED
|
@@ -11,6 +11,34 @@ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
|
|
11
11
|
# Navigate to the mcp-server directory
|
|
12
12
|
cd "$SCRIPT_DIR"
|
|
13
13
|
|
|
14
|
+
# CRITICAL: Load .env file from project root if it exists
|
|
15
|
+
# This ensures the MCP server uses the same settings as other scripts
|
|
16
|
+
if [ -f "../.env" ]; then
|
|
17
|
+
echo "[DEBUG] Loading .env file from project root" >&2
|
|
18
|
+
set -a # Export all variables
|
|
19
|
+
source ../.env
|
|
20
|
+
set +a # Stop exporting
|
|
21
|
+
else
|
|
22
|
+
echo "[DEBUG] No .env file found, using defaults" >&2
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
# Set smart defaults if not already set
|
|
26
|
+
# These match what the CLI setup wizard uses
|
|
27
|
+
if [ -z "$QDRANT_URL" ]; then
|
|
28
|
+
export QDRANT_URL="http://localhost:6333"
|
|
29
|
+
echo "[DEBUG] Using default QDRANT_URL: $QDRANT_URL" >&2
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
if [ -z "$PREFER_LOCAL_EMBEDDINGS" ]; then
|
|
33
|
+
export PREFER_LOCAL_EMBEDDINGS="true"
|
|
34
|
+
echo "[DEBUG] Using default PREFER_LOCAL_EMBEDDINGS: true (privacy-first)" >&2
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
if [ -z "$ENABLE_MEMORY_DECAY" ]; then
|
|
38
|
+
export ENABLE_MEMORY_DECAY="false"
|
|
39
|
+
echo "[DEBUG] Using default ENABLE_MEMORY_DECAY: false" >&2
|
|
40
|
+
fi
|
|
41
|
+
|
|
14
42
|
# Check if virtual environment exists
|
|
15
43
|
if [ ! -d "venv" ]; then
|
|
16
44
|
echo "Creating virtual environment..."
|
|
@@ -63,11 +91,27 @@ if [ -z "$FASTEMBED_SKIP_HUGGINGFACE" ]; then
|
|
|
63
91
|
fi
|
|
64
92
|
|
|
65
93
|
# Debug: Show what environment variables are being passed
|
|
66
|
-
echo "[DEBUG] Environment variables for MCP server:"
|
|
67
|
-
echo "[DEBUG] VOYAGE_KEY: ${VOYAGE_KEY:+set}"
|
|
68
|
-
echo "[DEBUG] PREFER_LOCAL_EMBEDDINGS: ${PREFER_LOCAL_EMBEDDINGS:-not set}"
|
|
69
|
-
echo "[DEBUG] QDRANT_URL: ${QDRANT_URL:-not set}"
|
|
70
|
-
echo "[DEBUG] ENABLE_MEMORY_DECAY: ${ENABLE_MEMORY_DECAY:-not set}"
|
|
94
|
+
echo "[DEBUG] Environment variables for MCP server:" >&2
|
|
95
|
+
echo "[DEBUG] VOYAGE_KEY: ${VOYAGE_KEY:+set}" >&2
|
|
96
|
+
echo "[DEBUG] PREFER_LOCAL_EMBEDDINGS: ${PREFER_LOCAL_EMBEDDINGS:-not set}" >&2
|
|
97
|
+
echo "[DEBUG] QDRANT_URL: ${QDRANT_URL:-not set}" >&2
|
|
98
|
+
echo "[DEBUG] ENABLE_MEMORY_DECAY: ${ENABLE_MEMORY_DECAY:-not set}" >&2
|
|
99
|
+
|
|
100
|
+
# Quick connectivity check for Qdrant
|
|
101
|
+
echo "[DEBUG] Checking Qdrant connectivity at $QDRANT_URL..." >&2
|
|
102
|
+
if command -v curl &> /dev/null; then
|
|
103
|
+
# Check root endpoint instead of /health which doesn't exist in Qdrant
|
|
104
|
+
if curl -s -f -m 2 "$QDRANT_URL/" > /dev/null 2>&1; then
|
|
105
|
+
echo "[DEBUG] ✅ Qdrant is reachable at $QDRANT_URL" >&2
|
|
106
|
+
else
|
|
107
|
+
echo "[WARNING] ⚠️ Cannot reach Qdrant at $QDRANT_URL" >&2
|
|
108
|
+
echo "[WARNING] Common fixes:" >&2
|
|
109
|
+
echo "[WARNING] 1. Start Qdrant: docker compose up -d qdrant" >&2
|
|
110
|
+
echo "[WARNING] 2. Check if port is different (e.g., 59999)" >&2
|
|
111
|
+
echo "[WARNING] 3. Update .env file with correct QDRANT_URL" >&2
|
|
112
|
+
echo "[WARNING] Continuing anyway - some features may not work..." >&2
|
|
113
|
+
fi
|
|
114
|
+
fi
|
|
71
115
|
|
|
72
116
|
# Run the MCP server
|
|
73
117
|
exec python -m src
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Application context for sharing state across modules."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Optional, Any
|
|
5
|
+
from qdrant_client import AsyncQdrantClient
|
|
6
|
+
try:
|
|
7
|
+
from .embedding_manager import EmbeddingManager
|
|
8
|
+
from .decay_manager import DecayManager
|
|
9
|
+
from .utils import ProjectResolver
|
|
10
|
+
except ImportError:
|
|
11
|
+
# Fallback for testing
|
|
12
|
+
EmbeddingManager = None
|
|
13
|
+
DecayManager = None
|
|
14
|
+
ProjectResolver = None
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class AppContext:
|
|
18
|
+
"""Shared application context for all MCP tools."""
|
|
19
|
+
|
|
20
|
+
qdrant_client: AsyncQdrantClient
|
|
21
|
+
embedding_manager: EmbeddingManager
|
|
22
|
+
decay_manager: DecayManager
|
|
23
|
+
project_resolver: ProjectResolver
|
|
24
|
+
|
|
25
|
+
# Optional context for debugging
|
|
26
|
+
debug_context: Optional[Any] = None
|
|
27
|
+
|
|
28
|
+
def __post_init__(self):
|
|
29
|
+
"""Initialize any additional state after dataclass creation."""
|
|
30
|
+
# Ensure all managers are properly initialized
|
|
31
|
+
if not self.embedding_manager:
|
|
32
|
+
self.embedding_manager = EmbeddingManager()
|
|
33
|
+
|
|
34
|
+
if not self.decay_manager:
|
|
35
|
+
self.decay_manager = DecayManager()
|
|
36
|
+
|
|
37
|
+
if not self.project_resolver:
|
|
38
|
+
self.project_resolver = ProjectResolver()
|
|
39
|
+
|
|
40
|
+
async def get_all_collections(self) -> list:
|
|
41
|
+
"""Get all collections from Qdrant."""
|
|
42
|
+
try:
|
|
43
|
+
collections = await self.qdrant_client.get_collections()
|
|
44
|
+
return [c.name for c in collections.collections]
|
|
45
|
+
except Exception as e:
|
|
46
|
+
if self.debug_context:
|
|
47
|
+
await self.debug_context.debug(f"Failed to get collections: {e}")
|
|
48
|
+
return []
|
|
49
|
+
|
|
50
|
+
async def generate_embedding(self, text: str, embedding_type: Optional[str] = None):
|
|
51
|
+
"""Generate embedding using the embedding manager."""
|
|
52
|
+
# The embedding_manager.embed method is synchronous, not async
|
|
53
|
+
embeddings = self.embedding_manager.embed(text, input_type="document")
|
|
54
|
+
if embeddings and len(embeddings) > 0:
|
|
55
|
+
return embeddings[0]
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
def get_current_project(self) -> Optional[str]:
|
|
59
|
+
"""Get current project from resolver."""
|
|
60
|
+
return self.project_resolver.get_current_project()
|
|
61
|
+
|
|
62
|
+
def normalize_project_name(self, project_name: str) -> str:
|
|
63
|
+
"""Normalize project name using resolver."""
|
|
64
|
+
return self.project_resolver.normalize_project_name(project_name)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Configuration and environment constants for Claude Self-Reflect MCP server."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from dotenv import load_dotenv
|
|
6
|
+
|
|
7
|
+
# Load environment variables
|
|
8
|
+
load_dotenv()
|
|
9
|
+
|
|
10
|
+
# API Keys
|
|
11
|
+
VOYAGE_API_KEY = os.getenv('VOYAGE_API_KEY', '')
|
|
12
|
+
QDRANT_URL = os.getenv('QDRANT_URL', 'http://localhost:6333')
|
|
13
|
+
|
|
14
|
+
# Embedding Configuration
|
|
15
|
+
PREFER_LOCAL_EMBEDDINGS = os.getenv('PREFER_LOCAL_EMBEDDINGS', 'true').lower() == 'true'
|
|
16
|
+
VOYAGE_MODEL = "voyage-3-lite"
|
|
17
|
+
LOCAL_MODEL = "sentence-transformers/all-MiniLM-L6-v2"
|
|
18
|
+
|
|
19
|
+
# Decay Configuration
|
|
20
|
+
USE_DECAY = os.getenv('USE_DECAY', 'false').lower() == 'true'
|
|
21
|
+
DECAY_SCALE_DAYS = int(os.getenv('DECAY_SCALE_DAYS', '90'))
|
|
22
|
+
DECAY_WEIGHT = float(os.getenv('DECAY_WEIGHT', '0.3'))
|
|
23
|
+
USE_NATIVE_DECAY = os.getenv('USE_NATIVE_DECAY', 'false').lower() == 'true'
|
|
24
|
+
|
|
25
|
+
# Search Configuration
|
|
26
|
+
DEFAULT_SEARCH_LIMIT = 5
|
|
27
|
+
MAX_SEARCH_LIMIT = 100
|
|
28
|
+
DEFAULT_MIN_SCORE = 0.3
|
|
29
|
+
|
|
30
|
+
# Memory Management
|
|
31
|
+
MAX_RESULTS_PER_COLLECTION = int(os.getenv('MAX_RESULTS_PER_COLLECTION', '10'))
|
|
32
|
+
MAX_TOTAL_RESULTS = int(os.getenv('MAX_TOTAL_RESULTS', '1000'))
|
|
33
|
+
MAX_MEMORY_MB = int(os.getenv('MAX_MEMORY_MB', '500'))
|
|
34
|
+
|
|
35
|
+
# Connection Pool Configuration
|
|
36
|
+
POOL_SIZE = int(os.getenv('QDRANT_POOL_SIZE', '10'))
|
|
37
|
+
POOL_MAX_OVERFLOW = int(os.getenv('QDRANT_POOL_OVERFLOW', '5'))
|
|
38
|
+
POOL_TIMEOUT = float(os.getenv('QDRANT_POOL_TIMEOUT', '30.0'))
|
|
39
|
+
RETRY_ATTEMPTS = int(os.getenv('QDRANT_RETRY_ATTEMPTS', '3'))
|
|
40
|
+
RETRY_DELAY = float(os.getenv('QDRANT_RETRY_DELAY', '1.0'))
|
|
41
|
+
|
|
42
|
+
# Performance Configuration
|
|
43
|
+
MAX_CONCURRENT_SEARCHES = int(os.getenv('MAX_CONCURRENT_SEARCHES', '10'))
|
|
44
|
+
ENABLE_PARALLEL_SEARCH = os.getenv('ENABLE_PARALLEL_SEARCH', 'true').lower() == 'true'
|
|
45
|
+
|
|
46
|
+
# Paths
|
|
47
|
+
CLAUDE_PROJECTS_PATH = Path.home() / '.claude' / 'projects'
|
|
48
|
+
CONFIG_PATH = Path.home() / '.claude-self-reflect' / 'config'
|
|
49
|
+
|
|
50
|
+
# Collection Naming
|
|
51
|
+
VOYAGE_SUFFIX = '_voyage'
|
|
52
|
+
LOCAL_SUFFIX = '_local'
|
|
53
|
+
|
|
54
|
+
# Logging
|
|
55
|
+
import logging
|
|
56
|
+
logging.basicConfig(level=logging.INFO)
|
|
57
|
+
logger = logging.getLogger(__name__)
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Connection pooling for Qdrant client to improve performance and resource management.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from typing import Optional, Any
|
|
7
|
+
from contextlib import asynccontextmanager
|
|
8
|
+
import logging
|
|
9
|
+
from qdrant_client import AsyncQdrantClient
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class QdrantConnectionPool:
|
|
15
|
+
"""
|
|
16
|
+
A connection pool for Qdrant clients with configurable size and timeout.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
url: str,
|
|
22
|
+
pool_size: int = 10,
|
|
23
|
+
max_overflow: int = 5,
|
|
24
|
+
timeout: float = 30.0,
|
|
25
|
+
retry_attempts: int = 3,
|
|
26
|
+
retry_delay: float = 1.0
|
|
27
|
+
):
|
|
28
|
+
"""
|
|
29
|
+
Initialize the connection pool.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
url: Qdrant server URL
|
|
33
|
+
pool_size: Base number of connections to maintain
|
|
34
|
+
max_overflow: Additional connections that can be created if pool is exhausted
|
|
35
|
+
timeout: Timeout for acquiring a connection from the pool
|
|
36
|
+
retry_attempts: Number of retry attempts for failed operations
|
|
37
|
+
retry_delay: Delay between retry attempts (with exponential backoff)
|
|
38
|
+
"""
|
|
39
|
+
self.url = url
|
|
40
|
+
self.pool_size = pool_size
|
|
41
|
+
self.max_overflow = max_overflow
|
|
42
|
+
self.timeout = timeout
|
|
43
|
+
self.retry_attempts = retry_attempts
|
|
44
|
+
self.retry_delay = retry_delay
|
|
45
|
+
|
|
46
|
+
# Connection pool
|
|
47
|
+
self._pool = asyncio.Queue(maxsize=pool_size)
|
|
48
|
+
self._overflow_connections = []
|
|
49
|
+
self._semaphore = asyncio.Semaphore(pool_size + max_overflow)
|
|
50
|
+
self._initialized = False
|
|
51
|
+
self._lock = asyncio.Lock()
|
|
52
|
+
|
|
53
|
+
# Statistics
|
|
54
|
+
self.stats = {
|
|
55
|
+
'connections_created': 0,
|
|
56
|
+
'connections_reused': 0,
|
|
57
|
+
'connections_failed': 0,
|
|
58
|
+
'overflow_used': 0,
|
|
59
|
+
'timeouts': 0
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async def initialize(self):
|
|
63
|
+
"""Initialize the connection pool with base connections."""
|
|
64
|
+
async with self._lock:
|
|
65
|
+
if self._initialized:
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
# Create initial pool connections
|
|
69
|
+
for _ in range(self.pool_size):
|
|
70
|
+
try:
|
|
71
|
+
client = AsyncQdrantClient(url=self.url)
|
|
72
|
+
await self._pool.put(client)
|
|
73
|
+
self.stats['connections_created'] += 1
|
|
74
|
+
except Exception as e:
|
|
75
|
+
logger.error(f"Failed to create initial connection: {e}")
|
|
76
|
+
self.stats['connections_failed'] += 1
|
|
77
|
+
|
|
78
|
+
self._initialized = True
|
|
79
|
+
logger.info(f"Connection pool initialized with {self._pool.qsize()} connections")
|
|
80
|
+
|
|
81
|
+
@asynccontextmanager
|
|
82
|
+
async def acquire(self):
|
|
83
|
+
"""
|
|
84
|
+
Acquire a connection from the pool.
|
|
85
|
+
|
|
86
|
+
Yields:
|
|
87
|
+
AsyncQdrantClient instance
|
|
88
|
+
"""
|
|
89
|
+
if not self._initialized:
|
|
90
|
+
await self.initialize()
|
|
91
|
+
|
|
92
|
+
client = None
|
|
93
|
+
acquired_from_overflow = False
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
# Try to get a connection with timeout
|
|
97
|
+
try:
|
|
98
|
+
client = await asyncio.wait_for(
|
|
99
|
+
self._pool.get(),
|
|
100
|
+
timeout=self.timeout
|
|
101
|
+
)
|
|
102
|
+
self.stats['connections_reused'] += 1
|
|
103
|
+
except asyncio.TimeoutError:
|
|
104
|
+
# Pool is exhausted, try overflow
|
|
105
|
+
self.stats['timeouts'] += 1
|
|
106
|
+
|
|
107
|
+
if len(self._overflow_connections) < self.max_overflow:
|
|
108
|
+
# Create overflow connection
|
|
109
|
+
logger.debug("Creating overflow connection")
|
|
110
|
+
client = AsyncQdrantClient(url=self.url)
|
|
111
|
+
self._overflow_connections.append(client)
|
|
112
|
+
acquired_from_overflow = True
|
|
113
|
+
self.stats['overflow_used'] += 1
|
|
114
|
+
self.stats['connections_created'] += 1
|
|
115
|
+
else:
|
|
116
|
+
raise RuntimeError("Connection pool exhausted and max overflow reached")
|
|
117
|
+
|
|
118
|
+
# Yield the client for use
|
|
119
|
+
yield client
|
|
120
|
+
|
|
121
|
+
finally:
|
|
122
|
+
# Return connection to pool
|
|
123
|
+
if client is not None:
|
|
124
|
+
if acquired_from_overflow:
|
|
125
|
+
# Remove from overflow list
|
|
126
|
+
if client in self._overflow_connections:
|
|
127
|
+
self._overflow_connections.remove(client)
|
|
128
|
+
else:
|
|
129
|
+
# Return to pool
|
|
130
|
+
try:
|
|
131
|
+
await self._pool.put(client)
|
|
132
|
+
except asyncio.QueueFull:
|
|
133
|
+
# This shouldn't happen, but handle gracefully
|
|
134
|
+
logger.warning("Connection pool is full, closing extra connection")
|
|
135
|
+
# In production, we might want to close the client here
|
|
136
|
+
|
|
137
|
+
async def execute_with_retry(self, func, *args, **kwargs):
|
|
138
|
+
"""
|
|
139
|
+
Execute a function with retry logic and exponential backoff.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
func: Async function to execute
|
|
143
|
+
*args: Positional arguments for the function
|
|
144
|
+
**kwargs: Keyword arguments for the function
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
Result from the function
|
|
148
|
+
"""
|
|
149
|
+
last_exception = None
|
|
150
|
+
delay = self.retry_delay
|
|
151
|
+
|
|
152
|
+
for attempt in range(self.retry_attempts):
|
|
153
|
+
try:
|
|
154
|
+
async with self.acquire() as client:
|
|
155
|
+
# Pass the client as the first argument
|
|
156
|
+
return await func(client, *args, **kwargs)
|
|
157
|
+
except Exception as e:
|
|
158
|
+
last_exception = e
|
|
159
|
+
if attempt < self.retry_attempts - 1:
|
|
160
|
+
logger.warning(f"Attempt {attempt + 1} failed: {e}. Retrying in {delay}s...")
|
|
161
|
+
await asyncio.sleep(delay)
|
|
162
|
+
delay *= 2 # Exponential backoff
|
|
163
|
+
else:
|
|
164
|
+
logger.error(f"All {self.retry_attempts} attempts failed: {e}")
|
|
165
|
+
|
|
166
|
+
raise last_exception
|
|
167
|
+
|
|
168
|
+
async def close(self):
|
|
169
|
+
"""Close all connections in the pool."""
|
|
170
|
+
async with self._lock:
|
|
171
|
+
# Close all pooled connections
|
|
172
|
+
while not self._pool.empty():
|
|
173
|
+
try:
|
|
174
|
+
client = await self._pool.get()
|
|
175
|
+
# AsyncQdrantClient doesn't have a close method, but we can del it
|
|
176
|
+
del client
|
|
177
|
+
except Exception as e:
|
|
178
|
+
logger.error(f"Error closing connection: {e}")
|
|
179
|
+
|
|
180
|
+
# Close overflow connections
|
|
181
|
+
for client in self._overflow_connections:
|
|
182
|
+
try:
|
|
183
|
+
del client
|
|
184
|
+
except Exception as e:
|
|
185
|
+
logger.error(f"Error closing overflow connection: {e}")
|
|
186
|
+
|
|
187
|
+
self._overflow_connections.clear()
|
|
188
|
+
self._initialized = False
|
|
189
|
+
logger.info("Connection pool closed")
|
|
190
|
+
|
|
191
|
+
def get_stats(self) -> dict:
|
|
192
|
+
"""Get pool statistics."""
|
|
193
|
+
return {
|
|
194
|
+
**self.stats,
|
|
195
|
+
'current_pool_size': self._pool.qsize() if self._initialized else 0,
|
|
196
|
+
'overflow_active': len(self._overflow_connections),
|
|
197
|
+
'initialized': self._initialized
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# Circuit breaker implementation for additional resilience
|
|
202
|
+
class CircuitBreaker:
|
|
203
|
+
"""
|
|
204
|
+
Circuit breaker pattern to prevent cascading failures.
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
def __init__(
|
|
208
|
+
self,
|
|
209
|
+
failure_threshold: int = 5,
|
|
210
|
+
recovery_timeout: float = 60.0,
|
|
211
|
+
expected_exception: type = Exception
|
|
212
|
+
):
|
|
213
|
+
"""
|
|
214
|
+
Initialize circuit breaker.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
failure_threshold: Number of failures before opening circuit
|
|
218
|
+
recovery_timeout: Time to wait before attempting recovery
|
|
219
|
+
expected_exception: Exception type to catch
|
|
220
|
+
"""
|
|
221
|
+
self.failure_threshold = failure_threshold
|
|
222
|
+
self.recovery_timeout = recovery_timeout
|
|
223
|
+
self.expected_exception = expected_exception
|
|
224
|
+
|
|
225
|
+
self._failure_count = 0
|
|
226
|
+
self._last_failure_time = None
|
|
227
|
+
self._state = 'closed' # closed, open, half_open
|
|
228
|
+
self._lock = asyncio.Lock()
|
|
229
|
+
|
|
230
|
+
async def call(self, func, *args, **kwargs):
|
|
231
|
+
"""
|
|
232
|
+
Call a function through the circuit breaker.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
func: Async function to call
|
|
236
|
+
*args: Positional arguments
|
|
237
|
+
**kwargs: Keyword arguments
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
Result from function
|
|
241
|
+
|
|
242
|
+
Raises:
|
|
243
|
+
CircuitBreakerOpen: If circuit is open
|
|
244
|
+
"""
|
|
245
|
+
async with self._lock:
|
|
246
|
+
# Check circuit state
|
|
247
|
+
if self._state == 'open':
|
|
248
|
+
# Check if we should try half-open
|
|
249
|
+
if self._last_failure_time:
|
|
250
|
+
time_since_failure = asyncio.get_event_loop().time() - self._last_failure_time
|
|
251
|
+
if time_since_failure > self.recovery_timeout:
|
|
252
|
+
self._state = 'half_open'
|
|
253
|
+
logger.info("Circuit breaker entering half-open state")
|
|
254
|
+
else:
|
|
255
|
+
raise CircuitBreakerOpen(f"Circuit breaker is open (failures: {self._failure_count})")
|
|
256
|
+
|
|
257
|
+
try:
|
|
258
|
+
# Attempt the call
|
|
259
|
+
result = await func(*args, **kwargs)
|
|
260
|
+
|
|
261
|
+
# Success - update state
|
|
262
|
+
async with self._lock:
|
|
263
|
+
if self._state == 'half_open':
|
|
264
|
+
self._state = 'closed'
|
|
265
|
+
logger.info("Circuit breaker closed after successful recovery")
|
|
266
|
+
self._failure_count = 0
|
|
267
|
+
self._last_failure_time = None
|
|
268
|
+
|
|
269
|
+
return result
|
|
270
|
+
|
|
271
|
+
except self.expected_exception as e:
|
|
272
|
+
# Failure - update state
|
|
273
|
+
async with self._lock:
|
|
274
|
+
self._failure_count += 1
|
|
275
|
+
self._last_failure_time = asyncio.get_event_loop().time()
|
|
276
|
+
|
|
277
|
+
if self._failure_count >= self.failure_threshold:
|
|
278
|
+
self._state = 'open'
|
|
279
|
+
logger.error(f"Circuit breaker opened after {self._failure_count} failures")
|
|
280
|
+
|
|
281
|
+
raise e
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
class CircuitBreakerOpen(Exception):
|
|
285
|
+
"""Exception raised when circuit breaker is open."""
|
|
286
|
+
pass
|