claude-self-reflect 7.0.0 → 7.1.9
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/csr-validator.md +87 -1
- package/.env.example +15 -0
- package/README.md +70 -1
- package/docker-compose.yaml +18 -9
- package/installer/setup-wizard-docker.js +87 -6
- package/installer/update-manager.js +88 -1
- package/mcp-server/src/reflection_tools.py +114 -2
- package/mcp-server/src/standalone_client.py +380 -0
- package/package.json +1 -1
- package/src/runtime/hooks/__init__.py +21 -0
- package/src/runtime/hooks/iteration_hook.py +196 -0
- package/src/runtime/hooks/ralph_state.py +402 -0
- package/src/runtime/hooks/session_end_hook.py +254 -0
- package/src/runtime/hooks/session_start_hook.py +259 -0
- package/src/runtime/precompact-hook.sh +82 -3
- package/src/runtime/unified_state_manager.py +35 -10
|
@@ -8,6 +8,7 @@ from typing import Optional, List, Dict, Any
|
|
|
8
8
|
from datetime import datetime, timezone
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
import uuid
|
|
11
|
+
from xml.sax.saxutils import escape as xml_escape
|
|
11
12
|
|
|
12
13
|
from fastmcp import Context
|
|
13
14
|
from pydantic import Field
|
|
@@ -207,13 +208,108 @@ Timestamp: {metadata['timestamp']}"""
|
|
|
207
208
|
<error>Conversation ID '{conversation_id}' not found in any project.</error>
|
|
208
209
|
<suggestion>The conversation may not have been imported yet, or the ID may be incorrect.</suggestion>
|
|
209
210
|
</conversation_file>"""
|
|
210
|
-
|
|
211
|
+
|
|
211
212
|
except Exception as e:
|
|
212
213
|
logger.error(f"Failed to get conversation file: {e}", exc_info=True)
|
|
213
214
|
return f"""<conversation_file>
|
|
214
215
|
<error>Failed to locate conversation: {str(e)}</error>
|
|
215
216
|
</conversation_file>"""
|
|
216
217
|
|
|
218
|
+
async def get_session_learnings(
|
|
219
|
+
self,
|
|
220
|
+
ctx: Context,
|
|
221
|
+
session_id: str,
|
|
222
|
+
limit: int = 50
|
|
223
|
+
) -> str:
|
|
224
|
+
"""Get all learnings from a specific Ralph session.
|
|
225
|
+
|
|
226
|
+
This enables iteration-level memory: retrieve what was learned
|
|
227
|
+
in previous iterations of the SAME Ralph loop session.
|
|
228
|
+
"""
|
|
229
|
+
from qdrant_client import models
|
|
230
|
+
|
|
231
|
+
await ctx.debug(f"Getting learnings for session: {session_id}")
|
|
232
|
+
|
|
233
|
+
try:
|
|
234
|
+
# Check runtime preference from environment
|
|
235
|
+
prefer_local = os.getenv('PREFER_LOCAL_EMBEDDINGS', 'true').lower() == 'true'
|
|
236
|
+
embedding_type = "local" if prefer_local else "voyage"
|
|
237
|
+
collection_name = f"reflections_{embedding_type}"
|
|
238
|
+
|
|
239
|
+
# Filter by session tag - matches reflections stored with session_{id} tag
|
|
240
|
+
session_filter = models.Filter(
|
|
241
|
+
must=[
|
|
242
|
+
models.FieldCondition(
|
|
243
|
+
key="tags",
|
|
244
|
+
match=models.MatchAny(any=[f"session_{session_id}"])
|
|
245
|
+
)
|
|
246
|
+
]
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
results, _ = await self.qdrant_client.scroll(
|
|
250
|
+
collection_name=collection_name,
|
|
251
|
+
scroll_filter=session_filter,
|
|
252
|
+
limit=limit,
|
|
253
|
+
with_payload=True,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
if not results:
|
|
257
|
+
await ctx.debug(f"No learnings found for session {session_id}")
|
|
258
|
+
return f"""<session_learnings>
|
|
259
|
+
<session_id>{session_id}</session_id>
|
|
260
|
+
<count>0</count>
|
|
261
|
+
<message>No learnings stored yet for this session. Use store_reflection() with tags=['session_{session_id}', 'iteration_N', 'ralph_iteration'] to store iteration learnings.</message>
|
|
262
|
+
</session_learnings>"""
|
|
263
|
+
|
|
264
|
+
# Format results
|
|
265
|
+
learnings = []
|
|
266
|
+
for point in results:
|
|
267
|
+
payload = point.payload or {}
|
|
268
|
+
tags = payload.get("tags", [])
|
|
269
|
+
# Extract iteration number from tags if present
|
|
270
|
+
iteration = "unknown"
|
|
271
|
+
for tag in tags:
|
|
272
|
+
if tag.startswith("iteration_"):
|
|
273
|
+
iteration = tag.replace("iteration_", "")
|
|
274
|
+
break
|
|
275
|
+
|
|
276
|
+
learnings.append({
|
|
277
|
+
"content": payload.get("content", ""),
|
|
278
|
+
"iteration": iteration,
|
|
279
|
+
"timestamp": payload.get("timestamp", ""),
|
|
280
|
+
"tags": tags
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
# Sort by timestamp (oldest first for chronological order)
|
|
284
|
+
learnings.sort(key=lambda x: x.get("timestamp", ""))
|
|
285
|
+
|
|
286
|
+
await ctx.debug(f"Found {len(learnings)} learnings for session {session_id}")
|
|
287
|
+
|
|
288
|
+
# Format as XML for structured output (escape special chars for safety)
|
|
289
|
+
learnings_xml = "\n".join([
|
|
290
|
+
f"""<learning iteration="{xml_escape(str(l['iteration']))}">
|
|
291
|
+
<timestamp>{xml_escape(str(l['timestamp']))}</timestamp>
|
|
292
|
+
<content>{xml_escape(str(l['content']))}</content>
|
|
293
|
+
<tags>{xml_escape(', '.join(str(t) for t in l['tags']))}</tags>
|
|
294
|
+
</learning>"""
|
|
295
|
+
for l in learnings
|
|
296
|
+
])
|
|
297
|
+
|
|
298
|
+
return f"""<session_learnings>
|
|
299
|
+
<session_id>{session_id}</session_id>
|
|
300
|
+
<count>{len(learnings)}</count>
|
|
301
|
+
<learnings>
|
|
302
|
+
{learnings_xml}
|
|
303
|
+
</learnings>
|
|
304
|
+
</session_learnings>"""
|
|
305
|
+
|
|
306
|
+
except Exception as e:
|
|
307
|
+
logger.error(f"Failed to get session learnings: {e}", exc_info=True)
|
|
308
|
+
return f"""<session_learnings>
|
|
309
|
+
<session_id>{session_id}</session_id>
|
|
310
|
+
<error>Failed to retrieve learnings: {str(e)}</error>
|
|
311
|
+
</session_learnings>"""
|
|
312
|
+
|
|
217
313
|
|
|
218
314
|
def register_reflection_tools(
|
|
219
315
|
mcp,
|
|
@@ -249,5 +345,21 @@ def register_reflection_tools(
|
|
|
249
345
|
"""Get the full JSONL conversation file path for a conversation ID.
|
|
250
346
|
This allows agents to read complete conversations instead of truncated excerpts."""
|
|
251
347
|
return await tools.get_full_conversation(ctx, conversation_id, project)
|
|
252
|
-
|
|
348
|
+
|
|
349
|
+
@mcp.tool()
|
|
350
|
+
async def get_session_learnings(
|
|
351
|
+
ctx: Context,
|
|
352
|
+
session_id: str = Field(description="Ralph session ID to get learnings from (e.g., 'ralph_20260104_224757_iter1')"),
|
|
353
|
+
limit: int = Field(default=50, description="Maximum number of learnings to return")
|
|
354
|
+
) -> str:
|
|
355
|
+
"""Get all learnings from a specific Ralph session.
|
|
356
|
+
|
|
357
|
+
This enables iteration-level memory: retrieve what was learned
|
|
358
|
+
in previous iterations of the SAME Ralph loop session.
|
|
359
|
+
|
|
360
|
+
Use this at the START of each Ralph iteration to see what previous
|
|
361
|
+
iterations learned. Then use store_reflection() with session tags
|
|
362
|
+
to save learnings at the END of each iteration."""
|
|
363
|
+
return await tools.get_session_learnings(ctx, session_id, limit)
|
|
364
|
+
|
|
253
365
|
logger.info("Reflection tools registered successfully")
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
CSR Standalone Client - For use outside the MCP server context.
|
|
4
|
+
|
|
5
|
+
This client provides search and store_reflection functionality
|
|
6
|
+
for hooks and scripts that need to interact with CSR without
|
|
7
|
+
going through the MCP protocol.
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
from mcp_server.src.standalone_client import CSRStandaloneClient
|
|
11
|
+
|
|
12
|
+
client = CSRStandaloneClient()
|
|
13
|
+
results = client.search("docker issues", limit=5)
|
|
14
|
+
client.store_reflection("Key insight here", tags=["insight"])
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import os
|
|
18
|
+
import uuid
|
|
19
|
+
import hashlib
|
|
20
|
+
import logging
|
|
21
|
+
from typing import List, Dict, Any, Optional
|
|
22
|
+
from datetime import datetime, timezone
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class CSRStandaloneClient:
|
|
29
|
+
"""Standalone CSR client for hooks and scripts."""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
qdrant_url: str = None,
|
|
34
|
+
qdrant_api_key: str = None,
|
|
35
|
+
prefer_local: bool = None
|
|
36
|
+
):
|
|
37
|
+
"""Initialize the standalone client.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
qdrant_url: Qdrant server URL (default: from env or localhost:6333)
|
|
41
|
+
qdrant_api_key: Qdrant API key (default: from env)
|
|
42
|
+
prefer_local: Use local embeddings (default: from env or True)
|
|
43
|
+
"""
|
|
44
|
+
self.qdrant_url = qdrant_url or os.getenv('QDRANT_URL', 'http://localhost:6333')
|
|
45
|
+
self.qdrant_api_key = qdrant_api_key or os.getenv('QDRANT_API_KEY')
|
|
46
|
+
|
|
47
|
+
if prefer_local is None:
|
|
48
|
+
self.prefer_local = os.getenv('PREFER_LOCAL_EMBEDDINGS', 'true').lower() == 'true'
|
|
49
|
+
else:
|
|
50
|
+
self.prefer_local = prefer_local
|
|
51
|
+
|
|
52
|
+
self._client = None
|
|
53
|
+
self._embedding_manager = None
|
|
54
|
+
|
|
55
|
+
def _get_client(self):
|
|
56
|
+
"""Get or create Qdrant client (synchronous)."""
|
|
57
|
+
if self._client is None:
|
|
58
|
+
from qdrant_client import QdrantClient
|
|
59
|
+
|
|
60
|
+
# Parse URL for host/port
|
|
61
|
+
import urllib.parse
|
|
62
|
+
parsed = urllib.parse.urlparse(self.qdrant_url)
|
|
63
|
+
host = parsed.hostname or 'localhost'
|
|
64
|
+
port = parsed.port or 6333
|
|
65
|
+
|
|
66
|
+
self._client = QdrantClient(
|
|
67
|
+
host=host,
|
|
68
|
+
port=port,
|
|
69
|
+
api_key=self.qdrant_api_key if self.qdrant_api_key else None,
|
|
70
|
+
timeout=30
|
|
71
|
+
)
|
|
72
|
+
return self._client
|
|
73
|
+
|
|
74
|
+
def _get_embedding_manager(self):
|
|
75
|
+
"""Get or create embedding manager."""
|
|
76
|
+
if self._embedding_manager is None:
|
|
77
|
+
# Try to use the project's embedding manager
|
|
78
|
+
try:
|
|
79
|
+
from .embedding_manager import EmbeddingManager
|
|
80
|
+
self._embedding_manager = EmbeddingManager()
|
|
81
|
+
except ImportError:
|
|
82
|
+
# Fallback: create minimal embedding functionality
|
|
83
|
+
self._embedding_manager = self._create_fallback_embeddings()
|
|
84
|
+
return self._embedding_manager
|
|
85
|
+
|
|
86
|
+
def _create_fallback_embeddings(self):
|
|
87
|
+
"""Create fallback embedding functionality using fastembed."""
|
|
88
|
+
class FallbackEmbeddings:
|
|
89
|
+
def __init__(self):
|
|
90
|
+
self._model = None
|
|
91
|
+
|
|
92
|
+
def get_model(self):
|
|
93
|
+
if self._model is None:
|
|
94
|
+
try:
|
|
95
|
+
from fastembed import TextEmbedding
|
|
96
|
+
self._model = TextEmbedding("BAAI/bge-small-en-v1.5")
|
|
97
|
+
except ImportError:
|
|
98
|
+
raise ImportError("fastembed not installed. Run: pip install fastembed")
|
|
99
|
+
return self._model
|
|
100
|
+
|
|
101
|
+
def embed(self, text: str) -> List[float]:
|
|
102
|
+
model = self.get_model()
|
|
103
|
+
embeddings = list(model.embed([text]))
|
|
104
|
+
return list(embeddings[0])
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def dimension(self) -> int:
|
|
108
|
+
return 384
|
|
109
|
+
|
|
110
|
+
return FallbackEmbeddings()
|
|
111
|
+
|
|
112
|
+
def search(
|
|
113
|
+
self,
|
|
114
|
+
query: str,
|
|
115
|
+
limit: int = 5,
|
|
116
|
+
min_score: float = 0.3,
|
|
117
|
+
project: str = None
|
|
118
|
+
) -> List[Dict[str, Any]]:
|
|
119
|
+
"""Search for relevant conversations.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
query: Search query
|
|
123
|
+
limit: Maximum results to return
|
|
124
|
+
min_score: Minimum similarity score
|
|
125
|
+
project: Project name filter (optional)
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
List of search results with content and metadata
|
|
129
|
+
"""
|
|
130
|
+
client = self._get_client()
|
|
131
|
+
embeddings = self._get_embedding_manager()
|
|
132
|
+
|
|
133
|
+
# Generate query embedding
|
|
134
|
+
query_vector = embeddings.embed(query)
|
|
135
|
+
|
|
136
|
+
# Find searchable collections
|
|
137
|
+
collections = client.get_collections().collections
|
|
138
|
+
searchable = [
|
|
139
|
+
c.name for c in collections
|
|
140
|
+
if self._is_searchable_collection(c.name)
|
|
141
|
+
]
|
|
142
|
+
|
|
143
|
+
if not searchable:
|
|
144
|
+
logger.warning("No searchable collections found")
|
|
145
|
+
return []
|
|
146
|
+
|
|
147
|
+
# Filter by project if specified
|
|
148
|
+
if project and project != 'all':
|
|
149
|
+
project_norm = self._normalize_project_name(project)
|
|
150
|
+
searchable = [c for c in searchable if project_norm in c]
|
|
151
|
+
|
|
152
|
+
# Prioritize reflections collections (where Ralph state is stored)
|
|
153
|
+
reflections = [c for c in searchable if c.startswith('reflections')]
|
|
154
|
+
others = [c for c in searchable if not c.startswith('reflections')]
|
|
155
|
+
searchable = reflections + others
|
|
156
|
+
|
|
157
|
+
results = []
|
|
158
|
+
for collection_name in searchable[:8]: # Search up to 8 collections
|
|
159
|
+
try:
|
|
160
|
+
search_results = client.search(
|
|
161
|
+
collection_name=collection_name,
|
|
162
|
+
query_vector=query_vector,
|
|
163
|
+
limit=limit,
|
|
164
|
+
score_threshold=min_score
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
for hit in search_results:
|
|
168
|
+
payload = hit.payload or {}
|
|
169
|
+
results.append({
|
|
170
|
+
'score': hit.score,
|
|
171
|
+
'content': payload.get('content', ''),
|
|
172
|
+
'preview': payload.get('preview', payload.get('content', '')[:200]),
|
|
173
|
+
'metadata': {
|
|
174
|
+
'collection': collection_name,
|
|
175
|
+
'conversation_id': payload.get('conversation_id', ''),
|
|
176
|
+
'timestamp': payload.get('timestamp', ''),
|
|
177
|
+
'project': payload.get('project', ''),
|
|
178
|
+
}
|
|
179
|
+
})
|
|
180
|
+
except Exception as e:
|
|
181
|
+
logger.debug(f"Error searching {collection_name}: {e}")
|
|
182
|
+
continue
|
|
183
|
+
|
|
184
|
+
# Sort by score and limit
|
|
185
|
+
results.sort(key=lambda x: x['score'], reverse=True)
|
|
186
|
+
return results[:limit]
|
|
187
|
+
|
|
188
|
+
def store_reflection(
|
|
189
|
+
self,
|
|
190
|
+
content: str,
|
|
191
|
+
tags: List[str] = None,
|
|
192
|
+
collection: str = None
|
|
193
|
+
) -> str:
|
|
194
|
+
"""Store a reflection/insight.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
content: The reflection content
|
|
198
|
+
tags: Optional tags for categorization
|
|
199
|
+
collection: Optional custom collection name (for hooks to use separate storage)
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
ID of stored reflection
|
|
203
|
+
"""
|
|
204
|
+
tags = tags or []
|
|
205
|
+
client = self._get_client()
|
|
206
|
+
embeddings = self._get_embedding_manager()
|
|
207
|
+
|
|
208
|
+
# Determine collection name
|
|
209
|
+
# Hooks can specify a custom collection to keep their data separate
|
|
210
|
+
if collection:
|
|
211
|
+
collection_name = collection
|
|
212
|
+
else:
|
|
213
|
+
collection_name = f"reflections_{'local' if self.prefer_local else 'voyage'}"
|
|
214
|
+
|
|
215
|
+
# Ensure collection exists
|
|
216
|
+
try:
|
|
217
|
+
client.get_collection(collection_name)
|
|
218
|
+
except Exception:
|
|
219
|
+
# Create collection
|
|
220
|
+
from qdrant_client.models import VectorParams, Distance
|
|
221
|
+
client.create_collection(
|
|
222
|
+
collection_name=collection_name,
|
|
223
|
+
vectors_config=VectorParams(
|
|
224
|
+
size=embeddings.dimension,
|
|
225
|
+
distance=Distance.COSINE
|
|
226
|
+
)
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
# Generate embedding
|
|
230
|
+
vector = embeddings.embed(content)
|
|
231
|
+
|
|
232
|
+
# Generate ID
|
|
233
|
+
reflection_id = hashlib.sha256(
|
|
234
|
+
f"{content}{datetime.now().isoformat()}".encode()
|
|
235
|
+
).hexdigest()[:16]
|
|
236
|
+
|
|
237
|
+
# Store
|
|
238
|
+
from qdrant_client.models import PointStruct
|
|
239
|
+
client.upsert(
|
|
240
|
+
collection_name=collection_name,
|
|
241
|
+
points=[
|
|
242
|
+
PointStruct(
|
|
243
|
+
id=str(uuid.uuid4()),
|
|
244
|
+
vector=vector,
|
|
245
|
+
payload={
|
|
246
|
+
"content": content,
|
|
247
|
+
"tags": tags,
|
|
248
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
249
|
+
"reflection_id": reflection_id,
|
|
250
|
+
"type": "reflection"
|
|
251
|
+
}
|
|
252
|
+
)
|
|
253
|
+
]
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
logger.info(f"Stored reflection: {reflection_id}")
|
|
257
|
+
return reflection_id
|
|
258
|
+
|
|
259
|
+
def _is_searchable_collection(self, name: str) -> bool:
|
|
260
|
+
"""Check if collection is searchable."""
|
|
261
|
+
return (
|
|
262
|
+
name.endswith('_local')
|
|
263
|
+
or name.endswith('_voyage')
|
|
264
|
+
or name.endswith('_384d')
|
|
265
|
+
or name.endswith('_1024d')
|
|
266
|
+
or '_cloud_' in name
|
|
267
|
+
or name.startswith('reflections')
|
|
268
|
+
or name.startswith('csr_')
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
def _normalize_project_name(self, name: str) -> str:
|
|
272
|
+
"""Normalize project name for collection matching."""
|
|
273
|
+
import re
|
|
274
|
+
# Convert to lowercase, replace special chars
|
|
275
|
+
normalized = name.lower()
|
|
276
|
+
normalized = re.sub(r'[^a-z0-9]', '_', normalized)
|
|
277
|
+
normalized = re.sub(r'_+', '_', normalized)
|
|
278
|
+
return normalized.strip('_')
|
|
279
|
+
|
|
280
|
+
def get_session_learnings(
|
|
281
|
+
self,
|
|
282
|
+
session_id: str,
|
|
283
|
+
limit: int = 50,
|
|
284
|
+
collection: str = None
|
|
285
|
+
) -> List[Dict[str, Any]]:
|
|
286
|
+
"""Get all learnings from a specific Ralph session.
|
|
287
|
+
|
|
288
|
+
This enables iteration-level memory: retrieve what was learned
|
|
289
|
+
in previous iterations of the SAME Ralph loop session.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
session_id: The session ID (e.g., "ralph_20260104_224757_iter1")
|
|
293
|
+
limit: Maximum number of reflections to return
|
|
294
|
+
collection: Optional custom collection (for hook-stored data)
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
List of reflection payloads from this session, each containing:
|
|
298
|
+
- content: The reflection text
|
|
299
|
+
- tags: List of tags (includes iteration info)
|
|
300
|
+
- timestamp: When it was stored
|
|
301
|
+
"""
|
|
302
|
+
from qdrant_client import models
|
|
303
|
+
|
|
304
|
+
client = self._get_client()
|
|
305
|
+
if collection:
|
|
306
|
+
collection_name = collection
|
|
307
|
+
else:
|
|
308
|
+
collection_name = f"reflections_{'local' if self.prefer_local else 'voyage'}"
|
|
309
|
+
|
|
310
|
+
# Filter by session tag - matches reflections stored with session_{id} tag
|
|
311
|
+
session_filter = models.Filter(
|
|
312
|
+
must=[
|
|
313
|
+
models.FieldCondition(
|
|
314
|
+
key="tags",
|
|
315
|
+
match=models.MatchAny(any=[f"session_{session_id}"])
|
|
316
|
+
)
|
|
317
|
+
]
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
try:
|
|
321
|
+
results, _ = client.scroll(
|
|
322
|
+
collection_name=collection_name,
|
|
323
|
+
scroll_filter=session_filter,
|
|
324
|
+
limit=limit,
|
|
325
|
+
with_payload=True,
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
return [
|
|
329
|
+
{
|
|
330
|
+
"content": point.payload.get("content", ""),
|
|
331
|
+
"tags": point.payload.get("tags", []),
|
|
332
|
+
"timestamp": point.payload.get("timestamp", ""),
|
|
333
|
+
}
|
|
334
|
+
for point in results
|
|
335
|
+
]
|
|
336
|
+
except Exception as e:
|
|
337
|
+
logger.error(f"Error getting session learnings: {e}")
|
|
338
|
+
return []
|
|
339
|
+
|
|
340
|
+
def test_connection(self) -> bool:
|
|
341
|
+
"""Test if CSR is accessible.
|
|
342
|
+
|
|
343
|
+
Returns:
|
|
344
|
+
True if connection successful
|
|
345
|
+
"""
|
|
346
|
+
try:
|
|
347
|
+
client = self._get_client()
|
|
348
|
+
client.get_collections()
|
|
349
|
+
return True
|
|
350
|
+
except Exception as e:
|
|
351
|
+
logger.error(f"Connection test failed: {e}")
|
|
352
|
+
return False
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
# Convenience function for quick searches
|
|
356
|
+
def quick_search(query: str, limit: int = 3) -> List[Dict[str, Any]]:
|
|
357
|
+
"""Quick search without creating client instance."""
|
|
358
|
+
client = CSRStandaloneClient()
|
|
359
|
+
return client.search(query, limit=limit)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
if __name__ == "__main__":
|
|
363
|
+
# Test the client
|
|
364
|
+
import sys
|
|
365
|
+
|
|
366
|
+
client = CSRStandaloneClient()
|
|
367
|
+
|
|
368
|
+
if client.test_connection():
|
|
369
|
+
print("✓ CSR connection successful")
|
|
370
|
+
|
|
371
|
+
if len(sys.argv) > 1:
|
|
372
|
+
query = " ".join(sys.argv[1:])
|
|
373
|
+
results = client.search(query, limit=3)
|
|
374
|
+
print(f"\nResults for '{query}':")
|
|
375
|
+
for i, r in enumerate(results, 1):
|
|
376
|
+
print(f"\n{i}. Score: {r['score']:.2f}")
|
|
377
|
+
print(f" {r['preview'][:100]}...")
|
|
378
|
+
else:
|
|
379
|
+
print("✗ CSR connection failed")
|
|
380
|
+
sys.exit(1)
|
package/package.json
CHANGED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Ralph memory hooks for Claude Code integration."""
|
|
2
|
+
|
|
3
|
+
from .ralph_state import (
|
|
4
|
+
RalphState,
|
|
5
|
+
load_state,
|
|
6
|
+
save_state,
|
|
7
|
+
is_ralph_session,
|
|
8
|
+
get_ralph_state_path,
|
|
9
|
+
load_ralph_session_state,
|
|
10
|
+
parse_ralph_wiggum_state,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
'RalphState',
|
|
15
|
+
'load_state',
|
|
16
|
+
'save_state',
|
|
17
|
+
'is_ralph_session',
|
|
18
|
+
'get_ralph_state_path',
|
|
19
|
+
'load_ralph_session_state',
|
|
20
|
+
'parse_ralph_wiggum_state',
|
|
21
|
+
]
|