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.
Files changed (50) hide show
  1. cli/__init__.py +1 -0
  2. cli/commands/__init__.py +1 -0
  3. cli/commands/benchmark.py +151 -0
  4. cli/commands/config_cmd.py +57 -0
  5. cli/commands/doctor.py +61 -0
  6. cli/commands/export.py +106 -0
  7. cli/commands/import_cmd.py +143 -0
  8. cli/commands/init.py +205 -0
  9. cli/commands/logs.py +38 -0
  10. cli/commands/monitor.py +63 -0
  11. cli/commands/plugins.py +37 -0
  12. cli/commands/start.py +24 -0
  13. cli/commands/status.py +92 -0
  14. cli/commands/stop.py +19 -0
  15. cli/commands/version.py +25 -0
  16. cli/commands/workspace.py +154 -0
  17. cli/main.py +496 -0
  18. cli/parser.py +188 -0
  19. cli_memory_os-0.1.0.dist-info/METADATA +231 -0
  20. cli_memory_os-0.1.0.dist-info/RECORD +50 -0
  21. cli_memory_os-0.1.0.dist-info/WHEEL +5 -0
  22. cli_memory_os-0.1.0.dist-info/entry_points.txt +2 -0
  23. cli_memory_os-0.1.0.dist-info/licenses/LICENSE +21 -0
  24. cli_memory_os-0.1.0.dist-info/top_level.txt +6 -0
  25. connectors/__init__.py +1 -0
  26. connectors/base.py +39 -0
  27. connectors/github.py +273 -0
  28. connectors/gmail.py +116 -0
  29. connectors/notion.py +156 -0
  30. connectors/registry.py +60 -0
  31. core/__init__.py +1 -0
  32. core/chunker.py +136 -0
  33. core/context_builder.py +407 -0
  34. core/embedder.py +42 -0
  35. core/llm.py +301 -0
  36. core/vector_store.py +629 -0
  37. infrastructure/__init__.py +1 -0
  38. infrastructure/compose.py +136 -0
  39. infrastructure/config.py +280 -0
  40. infrastructure/docker.py +61 -0
  41. infrastructure/health.py +338 -0
  42. infrastructure/observability.py +160 -0
  43. infrastructure/workspace.py +152 -0
  44. models/__init__.py +1 -0
  45. models/memory.py +28 -0
  46. storage/__init__.py +1 -0
  47. storage/db.py +528 -0
  48. storage/graph.py +324 -0
  49. storage/schema.sql +57 -0
  50. 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
+ }