utim-cli 1.0.0__py3-none-any.whl
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.
- utim_cli/__init__.py +40 -0
- utim_cli/agent.py +359 -0
- utim_cli/auth.py +208 -0
- utim_cli/backup.py +101 -0
- utim_cli/billing.py +40 -0
- utim_cli/blender_agent.py +1018 -0
- utim_cli/bootstrap.py +324 -0
- utim_cli/client_utils.py +135 -0
- utim_cli/config.py +194 -0
- utim_cli/context_pruner.py +504 -0
- utim_cli/doctor.py +118 -0
- utim_cli/knowledge_graph.py +462 -0
- utim_cli/logger.py +121 -0
- utim_cli/mcp_clean_wrapper.py +55 -0
- utim_cli/mcp_client.py +198 -0
- utim_cli/mcp_registry.json +1102 -0
- utim_cli/orchestrator.py +3209 -0
- utim_cli/reflection.py +200 -0
- utim_cli/report.py +100 -0
- utim_cli/scrapy_search.py +229 -0
- utim_cli/share.py +320 -0
- utim_cli/share_tui.py +554 -0
- utim_cli/situational_scoring.py +269 -0
- utim_cli/state.py +15 -0
- utim_cli/tools.py +3381 -0
- utim_cli/utim.py +4051 -0
- utim_cli/vector_memory.py +629 -0
- utim_cli/workspace.py +33 -0
- utim_cli-1.0.0.dist-info/METADATA +134 -0
- utim_cli-1.0.0.dist-info/RECORD +34 -0
- utim_cli-1.0.0.dist-info/WHEEL +5 -0
- utim_cli-1.0.0.dist-info/entry_points.txt +2 -0
- utim_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
- utim_cli-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,629 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Semantic Vector Memory — Enhanced RAG using ChromaDB with all-MiniLM-L6-v2 embeddings.
|
|
3
|
+
|
|
4
|
+
This module provides semantic search capabilities that understand meaning and context,
|
|
5
|
+
going beyond exact keyword matching to find conceptually related code.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import warnings
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
from dotenv import load_dotenv
|
|
13
|
+
load_dotenv()
|
|
14
|
+
except ImportError:
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
# Suppress HuggingFace and SentenceTransformers warnings and progress bars
|
|
18
|
+
os.environ["HF_HUB_DISABLE_PROGRESS_BARS"] = "1"
|
|
19
|
+
os.environ["TRANSFORMERS_VERBOSITY"] = "error"
|
|
20
|
+
os.environ["TOKENIZERS_PARALLELISM"] = "false"
|
|
21
|
+
|
|
22
|
+
import logging
|
|
23
|
+
logging.getLogger("huggingface_hub").setLevel(logging.ERROR)
|
|
24
|
+
logging.getLogger("httpx").setLevel(logging.ERROR)
|
|
25
|
+
logging.getLogger("httpcore").setLevel(logging.ERROR)
|
|
26
|
+
logging.getLogger("sentence_transformers").setLevel(logging.ERROR)
|
|
27
|
+
logging.getLogger("chromadb").setLevel(logging.ERROR)
|
|
28
|
+
warnings.filterwarnings("ignore", message=".*unauthenticated requests.*")
|
|
29
|
+
warnings.filterwarnings("ignore", module="huggingface_hub.*")
|
|
30
|
+
|
|
31
|
+
import json
|
|
32
|
+
import hashlib
|
|
33
|
+
from typing import List, Dict, Optional, Tuple
|
|
34
|
+
import numpy as np
|
|
35
|
+
|
|
36
|
+
# Enable ChromaDB and Hugging Face vector memory embeddings
|
|
37
|
+
try:
|
|
38
|
+
import chromadb
|
|
39
|
+
import chromadb.utils.embedding_functions as embedding_functions
|
|
40
|
+
CHROMA_AVAILABLE = True
|
|
41
|
+
except ImportError:
|
|
42
|
+
CHROMA_AVAILABLE = False
|
|
43
|
+
|
|
44
|
+
# Vector DB path
|
|
45
|
+
VECTOR_DB_PATH = ".utim_tmp/vector_db"
|
|
46
|
+
METADATA_FILE = ".utim_tmp/vector_meta.json"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class DeterministicMockEmbeddingFunction:
|
|
50
|
+
"""Fallback 100% offline mock embedding function."""
|
|
51
|
+
def __init__(self):
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def name(cls) -> str:
|
|
56
|
+
return "DeterministicMockEmbeddingFunction"
|
|
57
|
+
|
|
58
|
+
def get_config(self) -> dict:
|
|
59
|
+
return {}
|
|
60
|
+
|
|
61
|
+
@staticmethod
|
|
62
|
+
def build_from_config(config: dict) -> 'DeterministicMockEmbeddingFunction':
|
|
63
|
+
return DeterministicMockEmbeddingFunction()
|
|
64
|
+
|
|
65
|
+
def __call__(self, input: List[str]) -> List[List[float]]:
|
|
66
|
+
import hashlib
|
|
67
|
+
embeddings = []
|
|
68
|
+
for text in input:
|
|
69
|
+
words = text.lower().split()
|
|
70
|
+
vec = [0.0] * 384
|
|
71
|
+
if words:
|
|
72
|
+
for w in words:
|
|
73
|
+
h = int(hashlib.md5(w.encode('utf-8')).hexdigest(), 16)
|
|
74
|
+
for i in range(4):
|
|
75
|
+
idx = (h >> (i * 8)) % 384
|
|
76
|
+
vec[idx] += 1.0
|
|
77
|
+
norm = sum(x*x for x in vec) ** 0.5
|
|
78
|
+
if norm > 0:
|
|
79
|
+
vec = [x / norm for x in vec]
|
|
80
|
+
embeddings.append(vec)
|
|
81
|
+
return embeddings
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class VectorMemory:
|
|
85
|
+
"""
|
|
86
|
+
Semantic vector memory using ChromaDB with Hugging Face embeddings (all-MiniLM-L6-v2).
|
|
87
|
+
Provides meaning-based search over codebase content, task reflections, and experiences.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
def __init__(self, collection_name: str = "codebase"):
|
|
91
|
+
self.client = None
|
|
92
|
+
self.collection = None
|
|
93
|
+
self.embedding_func = None
|
|
94
|
+
self.file_metadata: Dict[str, Dict] = {}
|
|
95
|
+
self.collection_name = collection_name
|
|
96
|
+
self.metadata_file = f".utim_tmp/vector_meta_{collection_name}.json"
|
|
97
|
+
|
|
98
|
+
if not CHROMA_AVAILABLE:
|
|
99
|
+
raise ImportError("chromadb is required for vector memory")
|
|
100
|
+
|
|
101
|
+
self._initialize()
|
|
102
|
+
|
|
103
|
+
def _initialize(self):
|
|
104
|
+
"""Initialize ChromaDB client and collection with Hugging Face embedding model."""
|
|
105
|
+
os.makedirs(VECTOR_DB_PATH, exist_ok=True)
|
|
106
|
+
|
|
107
|
+
# Create persistent client
|
|
108
|
+
self.client = chromadb.PersistentClient(path=VECTOR_DB_PATH)
|
|
109
|
+
|
|
110
|
+
# Initialize Hugging Face embeddings
|
|
111
|
+
try:
|
|
112
|
+
self.embedding_func = embedding_functions.DefaultEmbeddingFunction()
|
|
113
|
+
except Exception:
|
|
114
|
+
try:
|
|
115
|
+
self.embedding_func = embedding_functions.SentenceTransformerEmbeddingFunction(model_name="all-MiniLM-L6-v2")
|
|
116
|
+
except Exception:
|
|
117
|
+
self.embedding_func = DeterministicMockEmbeddingFunction()
|
|
118
|
+
|
|
119
|
+
# Get or create collection
|
|
120
|
+
try:
|
|
121
|
+
self.collection = self.client.get_collection(
|
|
122
|
+
name=self.collection_name,
|
|
123
|
+
embedding_function=self.embedding_func
|
|
124
|
+
)
|
|
125
|
+
except Exception:
|
|
126
|
+
self.collection = self.client.create_collection(
|
|
127
|
+
name=self.collection_name,
|
|
128
|
+
embedding_function=self.embedding_func
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# Load metadata
|
|
132
|
+
self._load_metadata()
|
|
133
|
+
|
|
134
|
+
def _load_metadata(self):
|
|
135
|
+
"""Load file metadata from disk."""
|
|
136
|
+
if os.path.exists(self.metadata_file):
|
|
137
|
+
try:
|
|
138
|
+
with open(self.metadata_file, "r", encoding="utf-8") as f:
|
|
139
|
+
self.file_metadata = json.load(f)
|
|
140
|
+
except Exception as e:
|
|
141
|
+
from utim_cli.logger import log_error
|
|
142
|
+
log_error("vector_memory", f"Failed to load vector metadata from {self.metadata_file}. Resetting metadata.", e)
|
|
143
|
+
self.file_metadata = {}
|
|
144
|
+
|
|
145
|
+
def _save_metadata(self):
|
|
146
|
+
"""Save file metadata to disk."""
|
|
147
|
+
os.makedirs(".utim_tmp", exist_ok=True)
|
|
148
|
+
with open(self.metadata_file, "w", encoding="utf-8") as f:
|
|
149
|
+
json.dump(self.file_metadata, f, indent=2)
|
|
150
|
+
|
|
151
|
+
def _get_file_hash(self, filepath: str) -> str:
|
|
152
|
+
"""Get MD5 hash of file for change detection."""
|
|
153
|
+
try:
|
|
154
|
+
with open(filepath, "rb") as f:
|
|
155
|
+
return hashlib.md5(f.read()).hexdigest()
|
|
156
|
+
except Exception as e:
|
|
157
|
+
from utim_cli.logger import log_warning
|
|
158
|
+
log_warning("vector_memory", f"Failed to get file hash for {filepath}", e)
|
|
159
|
+
return ""
|
|
160
|
+
|
|
161
|
+
def sync_files(self, paths: List[str] = None, exclude_dirs: List[str] = None) -> int:
|
|
162
|
+
"""
|
|
163
|
+
Sync files to vector database.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
paths: List of file paths to sync. If None, walks current directory.
|
|
167
|
+
exclude_dirs: Directories to exclude from sync.
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
Number of files indexed/updated.
|
|
171
|
+
"""
|
|
172
|
+
if exclude_dirs is None:
|
|
173
|
+
exclude_dirs = {".git", "node_modules", "dist", "build", "__pycache__", ".venv", "venv", ".utim_tmp"}
|
|
174
|
+
else:
|
|
175
|
+
exclude_dirs = set(exclude_dirs)
|
|
176
|
+
|
|
177
|
+
files_to_sync = []
|
|
178
|
+
|
|
179
|
+
if paths:
|
|
180
|
+
files_to_sync = [(p, os.path.getmtime(p)) for p in paths if os.path.exists(p)]
|
|
181
|
+
else:
|
|
182
|
+
for root, dirs, files in os.walk("."):
|
|
183
|
+
dirs[:] = [d for d in dirs if d not in exclude_dirs]
|
|
184
|
+
for f in files:
|
|
185
|
+
ext = os.path.splitext(f)[1].lower()
|
|
186
|
+
if ext in ['.png', '.jpg', '.jpeg', '.gif', '.mp4', '.pdf', '.zip', '.exe', '.dll', '.pyc']:
|
|
187
|
+
continue
|
|
188
|
+
p = os.path.join(root, f)
|
|
189
|
+
try:
|
|
190
|
+
files_to_sync.append((p, os.path.getmtime(p)))
|
|
191
|
+
except Exception as e:
|
|
192
|
+
from utim_cli.logger import log_warning
|
|
193
|
+
log_warning("vector_memory", f"Failed to get modification time for {p}", e)
|
|
194
|
+
|
|
195
|
+
# Determine which files need updating
|
|
196
|
+
to_index = []
|
|
197
|
+
to_delete_ids = []
|
|
198
|
+
|
|
199
|
+
current_hashes = {}
|
|
200
|
+
for filepath, mtime in files_to_sync:
|
|
201
|
+
file_hash = self._get_file_hash(filepath)
|
|
202
|
+
current_hashes[filepath] = file_hash
|
|
203
|
+
|
|
204
|
+
existing = self.file_metadata.get(filepath, {})
|
|
205
|
+
if (filepath not in self.file_metadata or
|
|
206
|
+
existing.get("hash") != file_hash):
|
|
207
|
+
to_index.append(filepath)
|
|
208
|
+
|
|
209
|
+
# Check for deleted files
|
|
210
|
+
for filepath in list(self.file_metadata.keys()):
|
|
211
|
+
if filepath not in current_hashes:
|
|
212
|
+
to_delete_ids.append(self.file_metadata[filepath].get("chunk_ids", []))
|
|
213
|
+
del self.file_metadata[filepath]
|
|
214
|
+
|
|
215
|
+
# Delete removed chunks
|
|
216
|
+
if to_delete_ids:
|
|
217
|
+
flat_ids = [id for ids in to_delete_ids for id in ids]
|
|
218
|
+
if flat_ids:
|
|
219
|
+
try:
|
|
220
|
+
self.collection.delete(ids=flat_ids)
|
|
221
|
+
except Exception as e:
|
|
222
|
+
from utim_cli.logger import log_error
|
|
223
|
+
log_error("vector_memory", f"Failed to delete chunks from collection {self.collection_name}", e)
|
|
224
|
+
|
|
225
|
+
# Index new/updated files
|
|
226
|
+
indexed_count = 0
|
|
227
|
+
new_metadata = {}
|
|
228
|
+
|
|
229
|
+
for filepath in to_index:
|
|
230
|
+
try:
|
|
231
|
+
with open(filepath, "r", encoding="utf-8") as f:
|
|
232
|
+
content = f.read()
|
|
233
|
+
|
|
234
|
+
file_hash = self._get_file_hash(filepath)
|
|
235
|
+
|
|
236
|
+
# Chunk content for better retrieval
|
|
237
|
+
chunks = self._chunk_content(content, filepath)
|
|
238
|
+
|
|
239
|
+
if chunks:
|
|
240
|
+
ids = []
|
|
241
|
+
documents = []
|
|
242
|
+
metadatas = []
|
|
243
|
+
|
|
244
|
+
for i, (chunk_text, line_start, line_end) in enumerate(chunks):
|
|
245
|
+
chunk_id = f"{filepath}:{line_start}-{line_end}"
|
|
246
|
+
ids.append(chunk_id)
|
|
247
|
+
documents.append(chunk_text)
|
|
248
|
+
metadatas.append({
|
|
249
|
+
"filepath": filepath,
|
|
250
|
+
"line_start": line_start,
|
|
251
|
+
"line_end": line_end,
|
|
252
|
+
"chunk_index": i
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
self.collection.add(
|
|
256
|
+
ids=ids,
|
|
257
|
+
documents=documents,
|
|
258
|
+
metadatas=metadatas
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
new_metadata[filepath] = {
|
|
262
|
+
"hash": file_hash,
|
|
263
|
+
"mtime": os.path.getmtime(filepath),
|
|
264
|
+
"chunk_ids": ids
|
|
265
|
+
}
|
|
266
|
+
indexed_count += 1
|
|
267
|
+
|
|
268
|
+
except Exception as e:
|
|
269
|
+
continue
|
|
270
|
+
|
|
271
|
+
self.file_metadata.update(new_metadata)
|
|
272
|
+
self._save_metadata()
|
|
273
|
+
|
|
274
|
+
return indexed_count
|
|
275
|
+
|
|
276
|
+
def _chunk_content(self, content: str, filepath: str, chunk_size: int = 1000, overlap: int = 100) -> List[Tuple[str, int, int]]:
|
|
277
|
+
"""
|
|
278
|
+
Chunk content into overlapping segments with line number tracking.
|
|
279
|
+
|
|
280
|
+
Returns list of (chunk_text, start_line, end_line) tuples.
|
|
281
|
+
"""
|
|
282
|
+
lines = content.split('\n')
|
|
283
|
+
chunks = []
|
|
284
|
+
|
|
285
|
+
if len(lines) <= chunk_size:
|
|
286
|
+
return [(content, 1, len(lines))]
|
|
287
|
+
|
|
288
|
+
for i in range(0, len(lines), chunk_size - overlap):
|
|
289
|
+
end = min(i + chunk_size, len(lines))
|
|
290
|
+
chunk_text = '\n'.join(lines[i:end])
|
|
291
|
+
chunks.append((chunk_text, i + 1, end))
|
|
292
|
+
|
|
293
|
+
return chunks
|
|
294
|
+
|
|
295
|
+
def query(self, query_text: str, n_results: int = 5, where: Dict = None) -> List[Dict]:
|
|
296
|
+
"""
|
|
297
|
+
Query the vector database for semantically similar content.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
query_text: Natural language query
|
|
301
|
+
n_results: Number of results to return
|
|
302
|
+
where: Optional metadata filter
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
List of result dictionaries with content, filepath, and similarity info.
|
|
306
|
+
"""
|
|
307
|
+
if not self.collection:
|
|
308
|
+
return []
|
|
309
|
+
|
|
310
|
+
try:
|
|
311
|
+
results = self.collection.query(
|
|
312
|
+
query_texts=[query_text],
|
|
313
|
+
n_results=n_results,
|
|
314
|
+
where=where
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
formatted_results = []
|
|
318
|
+
if results["documents"] and results["documents"][0]:
|
|
319
|
+
for i, doc in enumerate(results["documents"][0]):
|
|
320
|
+
formatted_results.append({
|
|
321
|
+
"id": results["ids"][0][i] if results["ids"] else None,
|
|
322
|
+
"content": doc,
|
|
323
|
+
"filepath": results["metadatas"][0][i].get("filepath", ""),
|
|
324
|
+
"line_start": results["metadatas"][0][i].get("line_start", 0),
|
|
325
|
+
"line_end": results["metadatas"][0][i].get("line_end", 0),
|
|
326
|
+
"metadata": results["metadatas"][0][i],
|
|
327
|
+
"distance": results["distances"][0][i] if results["distances"] else None
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
return formatted_results
|
|
331
|
+
|
|
332
|
+
except Exception as e:
|
|
333
|
+
return []
|
|
334
|
+
|
|
335
|
+
def add_text(self, text_id: str, content: str, metadata: Dict = None) -> bool:
|
|
336
|
+
"""Add a single piece of text directly to the vector database."""
|
|
337
|
+
if not self.collection:
|
|
338
|
+
return False
|
|
339
|
+
|
|
340
|
+
try:
|
|
341
|
+
# Try to update if it exists
|
|
342
|
+
existing = self.collection.get(ids=[text_id])
|
|
343
|
+
if existing and existing["ids"]:
|
|
344
|
+
self.collection.update(
|
|
345
|
+
ids=[text_id],
|
|
346
|
+
documents=[content],
|
|
347
|
+
metadatas=[metadata or {}]
|
|
348
|
+
)
|
|
349
|
+
else:
|
|
350
|
+
self.collection.add(
|
|
351
|
+
ids=[text_id],
|
|
352
|
+
documents=[content],
|
|
353
|
+
metadatas=[metadata or {}]
|
|
354
|
+
)
|
|
355
|
+
return True
|
|
356
|
+
except Exception as e:
|
|
357
|
+
return False
|
|
358
|
+
|
|
359
|
+
def get_stats(self) -> Dict:
|
|
360
|
+
"""Get statistics about the vector database."""
|
|
361
|
+
if not self.collection:
|
|
362
|
+
return {"total_files": 0, "total_chunks": 0}
|
|
363
|
+
|
|
364
|
+
try:
|
|
365
|
+
count = self.collection.count()
|
|
366
|
+
return {
|
|
367
|
+
"total_files": len(self.file_metadata),
|
|
368
|
+
"total_chunks": count,
|
|
369
|
+
"db_path": VECTOR_DB_PATH
|
|
370
|
+
}
|
|
371
|
+
except Exception as e:
|
|
372
|
+
from utim_cli.logger import log_error
|
|
373
|
+
log_error("vector_memory", f"Failed to get stats for collection {self.collection_name}", e)
|
|
374
|
+
return {"total_files": 0, "total_chunks": 0}
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
# Global instance
|
|
378
|
+
_vector_memory: Optional[VectorMemory] = None
|
|
379
|
+
_experiences_memory: Optional[VectorMemory] = None
|
|
380
|
+
_skills_memory: Optional[VectorMemory] = None
|
|
381
|
+
|
|
382
|
+
# Sub-agent global instances
|
|
383
|
+
_project_res_experiences: Optional[VectorMemory] = None
|
|
384
|
+
_project_res_skills: Optional[VectorMemory] = None
|
|
385
|
+
_plan_project_experiences: Optional[VectorMemory] = None
|
|
386
|
+
_plan_project_skills: Optional[VectorMemory] = None
|
|
387
|
+
_web_search_experiences: Optional[VectorMemory] = None
|
|
388
|
+
_web_search_skills: Optional[VectorMemory] = None
|
|
389
|
+
_generate_image_experiences: Optional[VectorMemory] = None
|
|
390
|
+
_generate_image_skills: Optional[VectorMemory] = None
|
|
391
|
+
|
|
392
|
+
# Time memory global instance
|
|
393
|
+
_time_memory: Optional[VectorMemory] = None
|
|
394
|
+
_user_memories: Optional[VectorMemory] = None
|
|
395
|
+
|
|
396
|
+
def get_vector_memory() -> VectorMemory:
|
|
397
|
+
"""Get or create the global vector memory instance for the codebase."""
|
|
398
|
+
global _vector_memory
|
|
399
|
+
if _vector_memory is None:
|
|
400
|
+
try:
|
|
401
|
+
_vector_memory = VectorMemory(collection_name="codebase")
|
|
402
|
+
except ImportError:
|
|
403
|
+
pass
|
|
404
|
+
return _vector_memory
|
|
405
|
+
|
|
406
|
+
def get_experiences_memory() -> VectorMemory:
|
|
407
|
+
"""Get or create the global vector memory instance for the experiences."""
|
|
408
|
+
global _experiences_memory
|
|
409
|
+
if _experiences_memory is None:
|
|
410
|
+
try:
|
|
411
|
+
_experiences_memory = VectorMemory(collection_name="experiences")
|
|
412
|
+
except ImportError:
|
|
413
|
+
pass
|
|
414
|
+
return _experiences_memory
|
|
415
|
+
|
|
416
|
+
def get_skills_memory() -> VectorMemory:
|
|
417
|
+
"""Get or create the global vector memory instance for skills/rules."""
|
|
418
|
+
global _skills_memory
|
|
419
|
+
if _skills_memory is None:
|
|
420
|
+
try:
|
|
421
|
+
_skills_memory = VectorMemory(collection_name="skills")
|
|
422
|
+
except ImportError:
|
|
423
|
+
pass
|
|
424
|
+
return _skills_memory
|
|
425
|
+
|
|
426
|
+
# Sub-agent getters
|
|
427
|
+
def get_project_res_experiences_memory() -> VectorMemory:
|
|
428
|
+
"""Get or create the experiences vector memory instance for the project_res sub-agent."""
|
|
429
|
+
global _project_res_experiences
|
|
430
|
+
if _project_res_experiences is None:
|
|
431
|
+
try:
|
|
432
|
+
_project_res_experiences = VectorMemory(collection_name="project_res_experiences")
|
|
433
|
+
except ImportError:
|
|
434
|
+
pass
|
|
435
|
+
return _project_res_experiences
|
|
436
|
+
|
|
437
|
+
def get_project_res_skills_memory() -> VectorMemory:
|
|
438
|
+
"""Get or create the skills vector memory instance for the project_res sub-agent."""
|
|
439
|
+
global _project_res_skills
|
|
440
|
+
if _project_res_skills is None:
|
|
441
|
+
try:
|
|
442
|
+
_project_res_skills = VectorMemory(collection_name="project_res_skills")
|
|
443
|
+
except ImportError:
|
|
444
|
+
pass
|
|
445
|
+
return _project_res_skills
|
|
446
|
+
|
|
447
|
+
def get_plan_project_experiences_memory() -> VectorMemory:
|
|
448
|
+
"""Get or create the experiences vector memory instance for the plan_project sub-agent."""
|
|
449
|
+
global _plan_project_experiences
|
|
450
|
+
if _plan_project_experiences is None:
|
|
451
|
+
try:
|
|
452
|
+
_plan_project_experiences = VectorMemory(collection_name="plan_project_experiences")
|
|
453
|
+
except ImportError:
|
|
454
|
+
pass
|
|
455
|
+
return _plan_project_experiences
|
|
456
|
+
|
|
457
|
+
def get_plan_project_skills_memory() -> VectorMemory:
|
|
458
|
+
"""Get or create the skills vector memory instance for the plan_project sub-agent."""
|
|
459
|
+
global _plan_project_skills
|
|
460
|
+
if _plan_project_skills is None:
|
|
461
|
+
try:
|
|
462
|
+
_plan_project_skills = VectorMemory(collection_name="plan_project_skills")
|
|
463
|
+
except ImportError:
|
|
464
|
+
pass
|
|
465
|
+
return _plan_project_skills
|
|
466
|
+
|
|
467
|
+
def get_web_search_experiences_memory() -> VectorMemory:
|
|
468
|
+
"""Get or create the experiences vector memory instance for the web_search sub-agent."""
|
|
469
|
+
global _web_search_experiences
|
|
470
|
+
if _web_search_experiences is None:
|
|
471
|
+
try:
|
|
472
|
+
_web_search_experiences = VectorMemory(collection_name="web_search_experiences")
|
|
473
|
+
except ImportError:
|
|
474
|
+
pass
|
|
475
|
+
return _web_search_experiences
|
|
476
|
+
|
|
477
|
+
def get_web_search_skills_memory() -> VectorMemory:
|
|
478
|
+
"""Get or create the skills vector memory instance for the web_search sub-agent."""
|
|
479
|
+
global _web_search_skills
|
|
480
|
+
if _web_search_skills is None:
|
|
481
|
+
try:
|
|
482
|
+
_web_search_skills = VectorMemory(collection_name="web_search_skills")
|
|
483
|
+
except ImportError:
|
|
484
|
+
pass
|
|
485
|
+
return _web_search_skills
|
|
486
|
+
|
|
487
|
+
def get_generate_image_experiences_memory() -> VectorMemory:
|
|
488
|
+
"""Get or create the experiences vector memory instance for the generate_image sub-agent."""
|
|
489
|
+
global _generate_image_experiences
|
|
490
|
+
if _generate_image_experiences is None:
|
|
491
|
+
try:
|
|
492
|
+
_generate_image_experiences = VectorMemory(collection_name="generate_image_experiences")
|
|
493
|
+
except ImportError:
|
|
494
|
+
pass
|
|
495
|
+
return _generate_image_experiences
|
|
496
|
+
|
|
497
|
+
def get_generate_image_skills_memory() -> VectorMemory:
|
|
498
|
+
"""Get or create the skills vector memory instance for the generate_image sub-agent."""
|
|
499
|
+
global _generate_image_skills
|
|
500
|
+
if _generate_image_skills is None:
|
|
501
|
+
try:
|
|
502
|
+
_generate_image_skills = VectorMemory(collection_name="generate_image_skills")
|
|
503
|
+
except ImportError:
|
|
504
|
+
pass
|
|
505
|
+
return _generate_image_skills
|
|
506
|
+
|
|
507
|
+
def get_time_memory() -> VectorMemory:
|
|
508
|
+
"""Get or create the vector memory instance for time tracking and performance history."""
|
|
509
|
+
global _time_memory
|
|
510
|
+
if _time_memory is None:
|
|
511
|
+
try:
|
|
512
|
+
_time_memory = VectorMemory(collection_name="time_memory")
|
|
513
|
+
except ImportError:
|
|
514
|
+
pass
|
|
515
|
+
return _time_memory
|
|
516
|
+
|
|
517
|
+
# Global reflection memory instance
|
|
518
|
+
_reflections_memory: Optional[VectorMemory] = None
|
|
519
|
+
|
|
520
|
+
def get_reflections_memory() -> VectorMemory:
|
|
521
|
+
"""Get or create the global vector memory instance for task reflections."""
|
|
522
|
+
global _reflections_memory
|
|
523
|
+
if _reflections_memory is None:
|
|
524
|
+
try:
|
|
525
|
+
_reflections_memory = VectorMemory(collection_name="reflections")
|
|
526
|
+
except Exception:
|
|
527
|
+
pass
|
|
528
|
+
return _reflections_memory
|
|
529
|
+
|
|
530
|
+
def warmup_embedding_model() -> bool:
|
|
531
|
+
"""
|
|
532
|
+
Eagerly load the Hugging Face all-MiniLM-L6-v2 embedding model into memory.
|
|
533
|
+
Call this at startup in a background thread so the first real encode is instant.
|
|
534
|
+
Returns True if model was successfully loaded, False otherwise.
|
|
535
|
+
"""
|
|
536
|
+
try:
|
|
537
|
+
vm = get_reflections_memory()
|
|
538
|
+
if vm and vm.embedding_func:
|
|
539
|
+
# Encode a dummy string — this forces the ONNX/SentenceTransformer
|
|
540
|
+
# model weights to be downloaded (if not cached) and loaded into RAM.
|
|
541
|
+
_ = vm.embedding_func(["utim warmup ping"])
|
|
542
|
+
return True
|
|
543
|
+
except Exception:
|
|
544
|
+
pass
|
|
545
|
+
# Also try to pre-warm situational scoring's shared embedding function
|
|
546
|
+
try:
|
|
547
|
+
from utim_cli.situational_scoring import get_embedding_fn
|
|
548
|
+
fn = get_embedding_fn()
|
|
549
|
+
if fn:
|
|
550
|
+
_ = fn(["utim warmup ping"])
|
|
551
|
+
except Exception:
|
|
552
|
+
pass
|
|
553
|
+
return False
|
|
554
|
+
|
|
555
|
+
def store_reflection(content: str, category: str = "general_reflection", task_prompt: str = "") -> bool:
|
|
556
|
+
"""Store a reflection or learned insight into the Hugging Face Reflection Vector DB."""
|
|
557
|
+
vm = get_reflections_memory()
|
|
558
|
+
if not vm:
|
|
559
|
+
return False
|
|
560
|
+
import uuid
|
|
561
|
+
from datetime import datetime
|
|
562
|
+
text_id = f"refl_{uuid.uuid4().hex[:12]}"
|
|
563
|
+
return vm.add_text(
|
|
564
|
+
text_id=text_id,
|
|
565
|
+
content=content,
|
|
566
|
+
metadata={
|
|
567
|
+
"category": category,
|
|
568
|
+
"timestamp": datetime.now().isoformat(),
|
|
569
|
+
"task_prompt": task_prompt[:150]
|
|
570
|
+
}
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
def fetch_relevant_experiences(query_text: str, top_k: int = 2) -> List[Dict]:
|
|
574
|
+
"""
|
|
575
|
+
Fetch semantically relevant reflections and learned rules using Hugging Face embeddings.
|
|
576
|
+
Applies situational parameter scoring to prioritize operational rules (e.g. command operators) matching the task.
|
|
577
|
+
"""
|
|
578
|
+
results = []
|
|
579
|
+
# 1. Query reflections vector collection
|
|
580
|
+
vm_ref = get_reflections_memory()
|
|
581
|
+
if vm_ref:
|
|
582
|
+
res = vm_ref.query(query_text=query_text, n_results=5)
|
|
583
|
+
results.extend([r for r in res if r.get("distance") is None or r.get("distance") < 1.2])
|
|
584
|
+
|
|
585
|
+
# 2. Query general experiences vector collection
|
|
586
|
+
vm_exp = get_experiences_memory()
|
|
587
|
+
if vm_exp:
|
|
588
|
+
res = vm_exp.query(query_text=query_text, n_results=5)
|
|
589
|
+
results.extend([r for r in res if r.get("distance") is None or r.get("distance") < 1.2])
|
|
590
|
+
|
|
591
|
+
# Deduplicate by content
|
|
592
|
+
seen = set()
|
|
593
|
+
unique_results = []
|
|
594
|
+
for item in results:
|
|
595
|
+
text = item.get("content", "").strip()
|
|
596
|
+
if text and text not in seen:
|
|
597
|
+
seen.add(text)
|
|
598
|
+
# Invert distance into base_score (closer distance = higher score)
|
|
599
|
+
dist = item.get("distance")
|
|
600
|
+
item["base_score"] = 1.0 / (1.0 + dist) if dist is not None else 1.0
|
|
601
|
+
unique_results.append(item)
|
|
602
|
+
|
|
603
|
+
# Apply situational scoring and filtering
|
|
604
|
+
try:
|
|
605
|
+
from utim_cli.situational_scoring import score_and_filter_context
|
|
606
|
+
scored_results = score_and_filter_context(unique_results, query_text, limit=top_k)
|
|
607
|
+
return scored_results
|
|
608
|
+
except Exception:
|
|
609
|
+
return unique_results[:top_k]
|
|
610
|
+
|
|
611
|
+
def reset_vector_memory():
|
|
612
|
+
"""Reset the global vector memory instances."""
|
|
613
|
+
global _vector_memory, _experiences_memory, _skills_memory, _reflections_memory
|
|
614
|
+
global _project_res_experiences, _project_res_skills, _plan_project_experiences, _plan_project_skills, _web_search_experiences, _web_search_skills, _generate_image_experiences, _generate_image_skills
|
|
615
|
+
global _time_memory, _user_memories
|
|
616
|
+
_vector_memory = None
|
|
617
|
+
_experiences_memory = None
|
|
618
|
+
_skills_memory = None
|
|
619
|
+
_reflections_memory = None
|
|
620
|
+
_project_res_experiences = None
|
|
621
|
+
_project_res_skills = None
|
|
622
|
+
_plan_project_experiences = None
|
|
623
|
+
_plan_project_skills = None
|
|
624
|
+
_web_search_experiences = None
|
|
625
|
+
_web_search_skills = None
|
|
626
|
+
_generate_image_experiences = None
|
|
627
|
+
_generate_image_skills = None
|
|
628
|
+
_time_memory = None
|
|
629
|
+
_user_memories = None
|
utim_cli/workspace.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
class WorkspaceManager:
|
|
4
|
+
def __init__(self, root_dir="."):
|
|
5
|
+
self.root_dir = root_dir
|
|
6
|
+
|
|
7
|
+
def list_files(self):
|
|
8
|
+
file_tree = []
|
|
9
|
+
for root, dirs, files in os.walk(self.root_dir):
|
|
10
|
+
if ".git" in dirs:
|
|
11
|
+
dirs.remove(".git")
|
|
12
|
+
for file in files:
|
|
13
|
+
file_tree.append(os.path.join(root, file))
|
|
14
|
+
return file_tree
|
|
15
|
+
|
|
16
|
+
def read_file(self, file_path):
|
|
17
|
+
full_path = os.path.join(self.root_dir, file_path)
|
|
18
|
+
if os.path.exists(full_path):
|
|
19
|
+
with open(full_path, "r") as f:
|
|
20
|
+
return f.read()
|
|
21
|
+
return None
|
|
22
|
+
|
|
23
|
+
def write_file(self, file_path, content):
|
|
24
|
+
full_path = os.path.join(self.root_dir, file_path)
|
|
25
|
+
os.makedirs(os.path.dirname(full_path), exist_ok=True)
|
|
26
|
+
with open(full_path, "w") as f:
|
|
27
|
+
f.write(content)
|
|
28
|
+
return True
|
|
29
|
+
|
|
30
|
+
def get_context(self):
|
|
31
|
+
"""Build a context string summarizing the workspace."""
|
|
32
|
+
files = self.list_files()
|
|
33
|
+
return f"Current Workspace Files: {', '.join(files[:20])}" # Limit for context size
|