cli-memory-os 0.1.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.
- cli/__init__.py +1 -0
- cli/commands/__init__.py +1 -0
- cli/commands/benchmark.py +151 -0
- cli/commands/config_cmd.py +57 -0
- cli/commands/doctor.py +61 -0
- cli/commands/export.py +106 -0
- cli/commands/import_cmd.py +143 -0
- cli/commands/init.py +205 -0
- cli/commands/logs.py +38 -0
- cli/commands/monitor.py +63 -0
- cli/commands/plugins.py +37 -0
- cli/commands/start.py +24 -0
- cli/commands/status.py +92 -0
- cli/commands/stop.py +19 -0
- cli/commands/version.py +25 -0
- cli/commands/workspace.py +154 -0
- cli/main.py +496 -0
- cli/parser.py +188 -0
- cli_memory_os-0.1.0.dist-info/METADATA +231 -0
- cli_memory_os-0.1.0.dist-info/RECORD +50 -0
- cli_memory_os-0.1.0.dist-info/WHEEL +5 -0
- cli_memory_os-0.1.0.dist-info/entry_points.txt +2 -0
- cli_memory_os-0.1.0.dist-info/licenses/LICENSE +21 -0
- cli_memory_os-0.1.0.dist-info/top_level.txt +6 -0
- connectors/__init__.py +1 -0
- connectors/base.py +39 -0
- connectors/github.py +273 -0
- connectors/gmail.py +116 -0
- connectors/notion.py +156 -0
- connectors/registry.py +60 -0
- core/__init__.py +1 -0
- core/chunker.py +136 -0
- core/context_builder.py +407 -0
- core/embedder.py +42 -0
- core/llm.py +301 -0
- core/vector_store.py +629 -0
- infrastructure/__init__.py +1 -0
- infrastructure/compose.py +136 -0
- infrastructure/config.py +280 -0
- infrastructure/docker.py +61 -0
- infrastructure/health.py +338 -0
- infrastructure/observability.py +160 -0
- infrastructure/workspace.py +152 -0
- models/__init__.py +1 -0
- models/memory.py +28 -0
- storage/__init__.py +1 -0
- storage/db.py +528 -0
- storage/graph.py +324 -0
- storage/schema.sql +57 -0
- storage/tech_detector.py +117 -0
core/llm.py
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import time
|
|
3
|
+
import logging
|
|
4
|
+
from groq import Groq
|
|
5
|
+
from core.vector_store import hybrid_search
|
|
6
|
+
from storage.graph import GraphStore
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger("llm")
|
|
9
|
+
|
|
10
|
+
_current_key_idx = 0
|
|
11
|
+
|
|
12
|
+
def get_groq_client_and_key():
|
|
13
|
+
global _current_key_idx
|
|
14
|
+
keys = []
|
|
15
|
+
|
|
16
|
+
key1 = os.getenv("GROQ_API_KEY_1")
|
|
17
|
+
key2 = os.getenv("GROQ_API_KEY_2")
|
|
18
|
+
if key1:
|
|
19
|
+
keys.append(key1)
|
|
20
|
+
if key2:
|
|
21
|
+
keys.append(key2)
|
|
22
|
+
|
|
23
|
+
if not keys:
|
|
24
|
+
fallback = os.getenv("GROQ_API_KEY")
|
|
25
|
+
if fallback:
|
|
26
|
+
keys.append(fallback)
|
|
27
|
+
|
|
28
|
+
if not keys:
|
|
29
|
+
raise ValueError("No Groq API keys found. Please set GROQ_API_KEY_1, GROQ_API_KEY_2, or GROQ_API_KEY.")
|
|
30
|
+
|
|
31
|
+
selected_key = keys[_current_key_idx % len(keys)]
|
|
32
|
+
return Groq(api_key=selected_key), selected_key
|
|
33
|
+
|
|
34
|
+
def rotate_groq_key():
|
|
35
|
+
global _current_key_idx
|
|
36
|
+
_current_key_idx += 1
|
|
37
|
+
logger.info("Rotated Groq API key.")
|
|
38
|
+
|
|
39
|
+
def query_llm_with_retry(messages: list, model: str = None, max_retries: int = 3) -> str:
|
|
40
|
+
if not model:
|
|
41
|
+
model = os.getenv("GROQ_MODEL", "llama-3.3-70b-versatile")
|
|
42
|
+
|
|
43
|
+
# Map legacy/unsupported model names to supported ones on Groq
|
|
44
|
+
mapped_model = model
|
|
45
|
+
if "/" in model:
|
|
46
|
+
mapped_model = model.split("/")[-1]
|
|
47
|
+
|
|
48
|
+
# Groq does not host gpt-oss-120b. Map it to a supported model.
|
|
49
|
+
if mapped_model in ["gpt-oss-120b", "openai/gpt-oss-120b", "openai-gpt-oss-120b"]:
|
|
50
|
+
mapped_model = "llama-3.3-70b-versatile"
|
|
51
|
+
|
|
52
|
+
# Make sure we don't crash if the mapped model is completely invalid
|
|
53
|
+
fallback_models = ["llama-3.3-70b-versatile", "mixtral-8x7b-32768", "llama-3.1-8b-instant"]
|
|
54
|
+
|
|
55
|
+
for attempt in range(max_retries):
|
|
56
|
+
try:
|
|
57
|
+
client, key = get_groq_client_and_key()
|
|
58
|
+
response = client.chat.completions.create(
|
|
59
|
+
model=mapped_model,
|
|
60
|
+
messages=messages,
|
|
61
|
+
temperature=0.0
|
|
62
|
+
)
|
|
63
|
+
return response.choices[0].message.content
|
|
64
|
+
except Exception as e:
|
|
65
|
+
err_str = str(e)
|
|
66
|
+
is_429 = "429" in err_str or "rate limit" in err_str.lower() or "tpm" in err_str.lower()
|
|
67
|
+
|
|
68
|
+
# If rate limited, rotate key and sleep
|
|
69
|
+
if is_429 and attempt < max_retries - 1:
|
|
70
|
+
print(f"[Warning] Groq rate limit hit. Rotating key and retrying (Attempt {attempt+1}/{max_retries})...")
|
|
71
|
+
rotate_groq_key()
|
|
72
|
+
time.sleep(1.0)
|
|
73
|
+
continue
|
|
74
|
+
|
|
75
|
+
# If the model is not found, try a fallback model
|
|
76
|
+
is_model_error = "model" in err_str.lower() or "not found" in err_str.lower()
|
|
77
|
+
if is_model_error and attempt < len(fallback_models):
|
|
78
|
+
old_model = mapped_model
|
|
79
|
+
mapped_model = fallback_models[attempt % len(fallback_models)]
|
|
80
|
+
print(f"[Warning] Model '{old_model}' failed. Trying fallback model '{mapped_model}'...")
|
|
81
|
+
time.sleep(0.5)
|
|
82
|
+
continue
|
|
83
|
+
|
|
84
|
+
# Re-raise on last attempt
|
|
85
|
+
if attempt == max_retries - 1:
|
|
86
|
+
raise e
|
|
87
|
+
time.sleep(0.5)
|
|
88
|
+
return ""
|
|
89
|
+
|
|
90
|
+
def run_hybrid_rag(question: str) -> dict:
|
|
91
|
+
import time
|
|
92
|
+
import logging
|
|
93
|
+
import re
|
|
94
|
+
logger = logging.getLogger("llm")
|
|
95
|
+
|
|
96
|
+
logger.info(f"Starting RAG pipeline for question: '{question}'")
|
|
97
|
+
start_time = time.perf_counter()
|
|
98
|
+
|
|
99
|
+
from core.vector_store import run_semantic_search, detect_repo_in_query
|
|
100
|
+
from storage.db import search_local_knowledge_ranked
|
|
101
|
+
from core.context_builder import ContextBuilder
|
|
102
|
+
|
|
103
|
+
# 1. Detect single repo mention
|
|
104
|
+
detected_repo = detect_repo_in_query(question)
|
|
105
|
+
|
|
106
|
+
# 2. Query Classification (Task 1)
|
|
107
|
+
query_class = ContextBuilder.classify_query(question, detected_repo)
|
|
108
|
+
|
|
109
|
+
# 3. Graph Guided Retrieval Filter (Task 6)
|
|
110
|
+
repo_filter = detected_repo
|
|
111
|
+
if query_class == "Technology Question":
|
|
112
|
+
# Extract tech and query graph for repo list filter
|
|
113
|
+
known_techs = ["python", "javascript", "typescript", "react", "node", "express", "mongodb", "fastapi", "postgresql", "tailwind", "docker", "kafka", "redis", "plotly", "gemini", "groq", "next.js", "firebase", "sqlite", "neo4j", "qdrant", "composio", "langchain", "sentence-transformer", "sentence-transformers"]
|
|
114
|
+
query_lower = question.lower()
|
|
115
|
+
matched_techs = []
|
|
116
|
+
for tech in known_techs:
|
|
117
|
+
patterns = [
|
|
118
|
+
r'\b' + re.escape(tech) + r'\b',
|
|
119
|
+
r'\b' + re.escape(tech.replace('.', '')) + r'\b',
|
|
120
|
+
r'\b' + re.escape(tech.replace('-', '')) + r'\b'
|
|
121
|
+
]
|
|
122
|
+
if any(re.search(pat, query_lower) for pat in patterns):
|
|
123
|
+
matched_techs.append(tech)
|
|
124
|
+
|
|
125
|
+
graph_repos = []
|
|
126
|
+
if matched_techs:
|
|
127
|
+
try:
|
|
128
|
+
graph = GraphStore()
|
|
129
|
+
for tech in matched_techs:
|
|
130
|
+
rels = graph.lookup_relationships(tech)
|
|
131
|
+
for r in rels:
|
|
132
|
+
if "Repository '" in r:
|
|
133
|
+
parts = r.split("Repository '")
|
|
134
|
+
if len(parts) > 1:
|
|
135
|
+
repo_name = parts[1].split("'")[0]
|
|
136
|
+
graph_repos.append(repo_name)
|
|
137
|
+
except Exception as e:
|
|
138
|
+
logger.error(f"Graph guided filtering query failed: {e}")
|
|
139
|
+
|
|
140
|
+
if graph_repos:
|
|
141
|
+
repo_filter = list(set(graph_repos))
|
|
142
|
+
|
|
143
|
+
# 4. Perform Retrieval
|
|
144
|
+
# Raw vector results from Qdrant
|
|
145
|
+
vector_results = run_semantic_search(question, limit=20, source_filter=None, raw_scores=True, repo_filter=repo_filter)
|
|
146
|
+
|
|
147
|
+
# Raw keyword results from SQLite
|
|
148
|
+
keyword_results = search_local_knowledge_ranked(question, repo_filter=repo_filter)
|
|
149
|
+
|
|
150
|
+
# Neo4j Graph Lookup
|
|
151
|
+
graph = GraphStore()
|
|
152
|
+
graph_results = graph.lookup_relationships(question)
|
|
153
|
+
|
|
154
|
+
# 5. Context Builder logic (Task 3, 4, 5)
|
|
155
|
+
builder = ContextBuilder()
|
|
156
|
+
formatted_chunks, sources, repos, num_vector, num_keyword, num_graph, after_dedup = builder.build_context(
|
|
157
|
+
question, vector_results, keyword_results, graph_results, repo_filter=repo_filter, query_class=query_class
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
system_prompt = (
|
|
161
|
+
"You are an assistant answering ONLY from the supplied retrieved context.\n\n"
|
|
162
|
+
"Never use external knowledge.\n\n"
|
|
163
|
+
"If the question specifies a repository, answer ONLY from chunks belonging to that repository.\n"
|
|
164
|
+
"Ignore unrelated repositories.\n\n"
|
|
165
|
+
"If the answer cannot be found in the supplied context, respond exactly:\n"
|
|
166
|
+
"\"I couldn't find that information in the indexed knowledge.\"\n\n"
|
|
167
|
+
"Do not invent information.\n\n"
|
|
168
|
+
"Mention technologies exactly as written.\n\n"
|
|
169
|
+
"Return:\n"
|
|
170
|
+
"Answer\n"
|
|
171
|
+
"Sources\n"
|
|
172
|
+
"Repositories Used"
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
user_prompt_template = (
|
|
176
|
+
"Retrieved Context:\n"
|
|
177
|
+
"---------------------\n"
|
|
178
|
+
"{context}\n"
|
|
179
|
+
"---------------------\n\n"
|
|
180
|
+
f"Question: {question}\n\n"
|
|
181
|
+
"Format the output strictly as:\n"
|
|
182
|
+
"Answer: [Provide the answer here]\n"
|
|
183
|
+
"Sources: [Provide the sources here]\n"
|
|
184
|
+
"Repositories Used: [Provide the repositories here]"
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
# Trim lowest ranked chunks if overall prompt exceeds token limits
|
|
188
|
+
merged_context, final_chunks_count = builder.trim_context_to_limit(
|
|
189
|
+
system_prompt, user_prompt_template, formatted_chunks
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
total_chars = len(merged_context)
|
|
193
|
+
est_prompt_tokens = builder.estimate_tokens(system_prompt + user_prompt_template.replace("{context}", merged_context))
|
|
194
|
+
|
|
195
|
+
# Print Retrieval Diagnostics in exact required format (Task 7)
|
|
196
|
+
repo_filter_str = str(repo_filter) if repo_filter else "None"
|
|
197
|
+
print("========================================")
|
|
198
|
+
print("QUERY CLASS")
|
|
199
|
+
print(query_class)
|
|
200
|
+
print("Repository Filter")
|
|
201
|
+
print(repo_filter_str)
|
|
202
|
+
print("Vector Candidates")
|
|
203
|
+
print(num_vector)
|
|
204
|
+
print("Keyword Candidates")
|
|
205
|
+
print(num_keyword)
|
|
206
|
+
print("Graph Candidates")
|
|
207
|
+
print(num_graph)
|
|
208
|
+
print("After Deduplication")
|
|
209
|
+
print(after_dedup)
|
|
210
|
+
print("Final Context Chunks")
|
|
211
|
+
print(final_chunks_count)
|
|
212
|
+
print("Context Characters")
|
|
213
|
+
print(total_chars)
|
|
214
|
+
print("Estimated Tokens")
|
|
215
|
+
print(est_prompt_tokens)
|
|
216
|
+
print("========================================")
|
|
217
|
+
|
|
218
|
+
# If context is completely empty, fail early to prevent hallucination
|
|
219
|
+
if not merged_context.strip():
|
|
220
|
+
logger.info("Empty RAG context. Skipping LLM query.")
|
|
221
|
+
duration = time.perf_counter() - start_time
|
|
222
|
+
print(f"Total RAG Pipeline Duration: {duration:.2f}s")
|
|
223
|
+
return {
|
|
224
|
+
"answer": "I couldn't find that information in the indexed knowledge.",
|
|
225
|
+
"sources": [],
|
|
226
|
+
"repositories": [],
|
|
227
|
+
"confidence": 0.0
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
messages = [
|
|
231
|
+
{"role": "system", "content": system_prompt},
|
|
232
|
+
{"role": "user", "content": user_prompt_template.replace("{context}", merged_context)}
|
|
233
|
+
]
|
|
234
|
+
|
|
235
|
+
try:
|
|
236
|
+
llm_start = time.perf_counter()
|
|
237
|
+
answer = query_llm_with_retry(messages).strip()
|
|
238
|
+
llm_duration = time.perf_counter() - llm_start
|
|
239
|
+
logger.info(f"Groq LLM generation finished in {llm_duration:.2f}s")
|
|
240
|
+
print(f"LLM Generation Duration: {llm_duration:.2f}s")
|
|
241
|
+
except Exception as e:
|
|
242
|
+
answer = f"Error communicating with Groq: {e}"
|
|
243
|
+
logger.error(f"Groq LLM generation failed: {e}")
|
|
244
|
+
|
|
245
|
+
# Calculate basic confidence based on whether the standard fallback text is returned
|
|
246
|
+
confidence = 0.9
|
|
247
|
+
if "i couldn't find that information" in answer.lower():
|
|
248
|
+
answer = "I couldn't find that information in the indexed knowledge."
|
|
249
|
+
confidence = 0.0
|
|
250
|
+
sources = []
|
|
251
|
+
repos = []
|
|
252
|
+
else:
|
|
253
|
+
# Try to parse the structured output
|
|
254
|
+
parsed_answer = ""
|
|
255
|
+
parsed_sources = []
|
|
256
|
+
parsed_repos = []
|
|
257
|
+
|
|
258
|
+
answer_marker = "Answer:"
|
|
259
|
+
sources_marker = "Sources:"
|
|
260
|
+
repos_marker = "Repositories Used:"
|
|
261
|
+
|
|
262
|
+
try:
|
|
263
|
+
if answer_marker in answer:
|
|
264
|
+
ans_start = answer.find(answer_marker) + len(answer_marker)
|
|
265
|
+
ans_end = answer.find(sources_marker) if sources_marker in answer else len(answer)
|
|
266
|
+
parsed_answer = answer[ans_start:ans_end].strip()
|
|
267
|
+
|
|
268
|
+
if sources_marker in answer:
|
|
269
|
+
src_start = answer.find(sources_marker) + len(sources_marker)
|
|
270
|
+
src_end = answer.find(repos_marker) if repos_marker in answer else len(answer)
|
|
271
|
+
srcs_str = answer[src_start:src_end].strip()
|
|
272
|
+
if srcs_str.lower() != "none" and srcs_str:
|
|
273
|
+
parsed_sources = [s.strip() for s in srcs_str.split(",") if s.strip()]
|
|
274
|
+
|
|
275
|
+
if repos_marker in answer:
|
|
276
|
+
rep_start = answer.find(repos_marker) + len(repos_marker)
|
|
277
|
+
rep_str = answer[rep_start:].strip()
|
|
278
|
+
if rep_str.lower() != "none" and rep_str:
|
|
279
|
+
parsed_repos = [r.strip() for r in rep_str.split(",") if r.strip()]
|
|
280
|
+
else:
|
|
281
|
+
parsed_answer = answer
|
|
282
|
+
except Exception:
|
|
283
|
+
parsed_answer = answer
|
|
284
|
+
|
|
285
|
+
if parsed_answer:
|
|
286
|
+
answer = parsed_answer
|
|
287
|
+
if parsed_sources:
|
|
288
|
+
sources = sorted(list(set(parsed_sources)))
|
|
289
|
+
if parsed_repos:
|
|
290
|
+
repos = sorted(list(set(parsed_repos)))
|
|
291
|
+
|
|
292
|
+
total_duration = time.perf_counter() - start_time
|
|
293
|
+
logger.info(f"RAG pipeline complete for question: '{question}' in {total_duration:.2f}s")
|
|
294
|
+
print(f"Total RAG Pipeline Duration: {total_duration:.2f}s")
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
"answer": answer,
|
|
298
|
+
"sources": sources,
|
|
299
|
+
"repositories": repos,
|
|
300
|
+
"confidence": confidence
|
|
301
|
+
}
|