diff --git a/Breify Note.pdf b/Breify Note.pdf deleted file mode 100644 index 8124454..0000000 Binary files a/Breify Note.pdf and /dev/null differ diff --git a/src/memora/ai/conversational_ai.py b/src/memora/ai/conversational_ai.py new file mode 100644 index 0000000..044c776 --- /dev/null +++ b/src/memora/ai/conversational_ai.py @@ -0,0 +1,90 @@ +import ollama +from pathlib import Path +from memora.core.ingestion import extract_facts +from memora.core.store import ObjectStore + + +class MemoraChat: + + def __init__(self): + self.client = ollama.Client() + self.store = ObjectStore(Path(".memora")) + + def extract_code_blocks(self, message: str): + code_blocks = [] + + if "```" in message: + parts = message.split("```") + for i in range(1, len(parts), 2): + block = parts[i].strip() + if block: + code_blocks.append(block) + + if not code_blocks: + if any(k in message for k in ["def ", "return ", "class ", "import "]): + code_blocks.append(message.strip()) + + return code_blocks + + def needs_memory(self, message: str) -> bool: + message = message.lower() + + triggers = [ + "my", "i", "me", + "what did i", "what do i", + "who am i", "remember", + "code", "earlier", "before" + ] + + return any(t in message for t in triggers) + + def chat(self, message: str): + + message_lower = message.lower() + + # ✅ STEP 1: store normal facts + if len(message.strip()) > 3: + facts = extract_facts(message, "user") + for f in facts: + self.store.write(f) + + # ✅ STEP 2: store code (SAFE METHOD) + code_blocks = self.extract_code_blocks(message) + + for code in code_blocks: + fact_text = f"user wrote code {code}" + facts = extract_facts(fact_text, "user") + + for f in facts: + self.store.write(f) + + # ✅ STEP 3: retrieve memory + if self.needs_memory(message): + + all_hashes = self.store.list_all_hashes() + + # 🔥 CODE RETRIEVAL + if "code" in message_lower: + for h in reversed(all_hashes): + try: + fact = self.store.read_fact(h) + + content = getattr(fact, "content", "").lower() + + if "code" in content or "def " in content: + return f"Here is your code:\n{fact.content}" + + except: + continue + + # ✅ STEP 4: fallback AI + try: + response = self.client.chat( + model="llama3.2", + messages=[{"role": "user", "content": message}] + ) + + return response["message"]["content"].strip() + + except Exception: + return "Something went wrong." \ No newline at end of file diff --git a/src/memora/ai/file_processor.py b/src/memora/ai/file_processor.py new file mode 100644 index 0000000..90d925e --- /dev/null +++ b/src/memora/ai/file_processor.py @@ -0,0 +1,32 @@ +from pathlib import Path +from memora.core.ingestion import extract_facts +from memora.core.store import ObjectStore + + +class FileProcessor: + + def __init__(self): + self.store = ObjectStore(Path(".memora")) + + def process_file(self, file_path: str): + + path = Path(file_path) + + if not path.exists(): + raise ValueError("File not found") + + # 1. Read file + if path.suffix == ".txt" or path.suffix == ".md": + with open(path, "r", encoding="utf-8") as f: + content = f.read() + else: + raise ValueError("Only .txt and .md supported") + + # 2. Extract facts + facts = extract_facts(content, f"file:{path.name}") + + # 3. Store facts + for f in facts: + self.store.write(f) + + return len(facts) \ No newline at end of file diff --git a/src/memora/core/store.py b/src/memora/core/store.py index 01c48ed..88d1c0d 100644 --- a/src/memora/core/store.py +++ b/src/memora/core/store.py @@ -32,53 +32,18 @@ class ObjectStore: - """Content-addressable object store with atomic writes and locking. - - Objects are stored at paths derived from their hash: - objects/{hash[:2]}/{hash[2:]} - - All writes are atomic (tmp file -> os.replace). - Write lock prevents concurrent modifications. - """ + """Content-addressable object store with atomic writes and locking.""" def __init__(self, store_path: Path): - """Initialize object store. - - Args: - store_path: Path to .memora/ directory - """ self.store_path = store_path self.objects_path = store_path / "objects" self.lock_path = store_path / ".write_lock" self._lock = FileLock(str(self.lock_path), timeout=30) def _object_path(self, hash: str) -> Path: - """Compute storage path for a hash. - - Uses Git-style two-character subdirectory to avoid - filesystem slowdown with many files in one directory. - - Args: - hash: 64-character hex string (SHA-256) - - Returns: - Path: objects/{hash[:2]}/{hash[2:]} - """ return self.objects_path / hash[:2] / hash[2:] def write(self, obj: Fact | MemoryTree | MemoryCommit) -> str: - """Write object to store atomically. - - Idempotent - writing the same object twice creates only one file. - Uses atomic write pattern (tmp -> os.replace) to prevent partial files. - - Args: - obj: Object to write (Fact, MemoryTree, or MemoryCommit) - - Returns: - SHA-256 hash of the object (64-char hex) - """ - # 1. Serialize object if isinstance(obj, Fact): compressed_bytes, obj_hash = serialize_fact(obj) elif isinstance(obj, MemoryTree): @@ -88,57 +53,31 @@ def write(self, obj: Fact | MemoryTree | MemoryCommit) -> str: else: raise TypeError(f"Unsupported object type: {type(obj)}") - # 2. Compute final path final_path = self._object_path(obj_hash) - # 3. Free deduplication - if exists, return immediately if final_path.exists(): return obj_hash - # 4. Create subdirectory final_path.parent.mkdir(parents=True, exist_ok=True) - # 5. Write to tmp file tmp_dir = self.objects_path / "tmp" tmp_dir.mkdir(parents=True, exist_ok=True) tmp_path = tmp_dir / str(uuid.uuid4()) - # 6. Write bytes tmp_path.write_bytes(compressed_bytes) - - # 7. Atomic move os.replace(str(tmp_path), str(final_path)) - # 8. Return hash return obj_hash def read_fact(self, hash: str) -> Fact: - """Read and verify a Fact from store. - - Args: - hash: SHA-256 hash of the fact - - Returns: - Fact object - - Raises: - ObjectNotFoundError: If hash not in store - HashMismatchError: If file content doesn't match hash (corruption) - """ - # 1. Compute path path = self._object_path(hash) - # 2. Check existence if not path.exists(): raise ObjectNotFoundError(f"Object not found: {hash[:8]}...") - # 3. Read bytes data = path.read_bytes() - - # 4. Deserialize fact = deserialize_fact(data) - # 5. Verify hash by recomputing from fact (semantic identity) actual_hash = hash_fact(fact) if actual_hash != hash: raise HashMismatchError( @@ -148,18 +87,6 @@ def read_fact(self, hash: str) -> Fact: return fact def read_tree(self, hash: str) -> MemoryTree: - """Read and verify a MemoryTree from store. - - Args: - hash: SHA-256 hash of the tree - - Returns: - MemoryTree object - - Raises: - ObjectNotFoundError: If hash not in store - HashMismatchError: If file content doesn't match hash (corruption) - """ path = self._object_path(hash) if not path.exists(): @@ -171,18 +98,6 @@ def read_tree(self, hash: str) -> MemoryTree: return deserialize_tree(data) def read_commit(self, hash: str) -> MemoryCommit: - """Read and verify a MemoryCommit from store. - - Args: - hash: SHA-256 hash of the commit - - Returns: - MemoryCommit object - - Raises: - ObjectNotFoundError: If hash not in store - HashMismatchError: If file content doesn't match hash (corruption) - """ path = self._object_path(hash) if not path.exists(): @@ -194,91 +109,59 @@ def read_commit(self, hash: str) -> MemoryCommit: return deserialize_commit(data) def exists(self, hash: str) -> bool: - """Fast check if object exists in store. - - Does not read or deserialize - just checks file existence. - - Args: - hash: SHA-256 hash to check - - Returns: - True if object exists, False otherwise - """ return self._object_path(hash).exists() def acquire_lock(self) -> None: - """Acquire write lock. - - Prevents concurrent writes to the store. - 30 second timeout. - - Raises: - StoreLockError: If lock cannot be acquired within timeout - """ try: self._lock.acquire() except Timeout: - raise StoreLockError("Write lock timeout. Another process may be writing.") + raise StoreLockError("Write lock timeout.") def release_lock(self) -> None: - """Release write lock. - - ALWAYS call in finally block to ensure lock is released - even if an error occurs. - """ try: self._lock.release() except Exception: - # Swallow exceptions - lock may already be released pass def list_all_hashes(self) -> list[str]: - """List all object hashes in the store. - - Walks the objects/ directory and reconstructs hashes - from subdirectory names and filenames. - - Skips the tmp/ subdirectory. - - Returns: - List of 64-character hash strings - """ hashes: list[str] = [] if not self.objects_path.exists(): return hashes - # Walk through two-character subdirectories for subdir in self.objects_path.iterdir(): - # Skip tmp directory if subdir.name == "tmp": continue - - # Skip if not a directory if not subdir.is_dir(): continue - # Each file in subdirectory is an object for obj_file in subdir.iterdir(): if obj_file.is_file(): - # Reconstruct hash: subdir_name + filename full_hash = subdir.name + obj_file.name - if len(full_hash) == 64: # Valid SHA-256 hash + if len(full_hash) == 64: hashes.append(full_hash) return hashes - @staticmethod - def initialize_directories(store_path: Path) -> None: - """Create complete .memora/ directory structure. + # ✅ FIXED: search method INSIDE class + def search(self, query: str) -> list[str]: + """Search facts by keyword.""" + results = [] - Creates all required directories and files for a new Memora store. - Safe to call on existing store - won't overwrite existing files. + all_hashes = self.list_all_hashes() - Args: - store_path: Path to .memora/ directory (will be created) - """ - # Create all required directories + for h in all_hashes: + try: + fact = self.read_fact(h) + if query.lower() in fact.content.lower(): + results.append(fact.content) + except: + pass + + return results + + @staticmethod + def initialize_directories(store_path: Path) -> None: directories = [ store_path / "objects", store_path / "objects" / "tmp", @@ -292,17 +175,14 @@ def initialize_directories(store_path: Path) -> None: for directory in directories: directory.mkdir(parents=True, exist_ok=True) - # Write HEAD file (only if doesn't exist) head_file = store_path / "HEAD" if not head_file.exists(): head_file.write_text("ref: refs/heads/main") - # Write empty STAGE file stage_file = store_path / "staging" / "STAGE" if not stage_file.exists(): stage_file.write_text("[]") - # Write empty index files entities_index = store_path / "index" / "entities.json" if not entities_index.exists(): entities_index.write_text("{}") @@ -315,7 +195,6 @@ def initialize_directories(store_path: Path) -> None: if not temporal_index.exists(): temporal_index.write_text("[]") - # Write config file config_file = store_path / "config" if not config_file.exists(): config = { @@ -323,10 +202,9 @@ def initialize_directories(store_path: Path) -> None: "max_context_tokens": 2000, "conflict_mode": "lenient", "default_branch": "main", - "api_key": secrets.token_hex(16), # 32-char hex string + "api_key": secrets.token_hex(16), } config_file.write_text(json.dumps(config, indent=2)) - # Set permissions on non-Windows if os.name != "nt": - os.chmod(store_path, 0o700) + os.chmod(store_path, 0o700) \ No newline at end of file diff --git a/src/memora/interface/cli.py b/src/memora/interface/cli.py index 2ca5c37..20f45b2 100644 --- a/src/memora/interface/cli.py +++ b/src/memora/interface/cli.py @@ -1,10 +1,27 @@ """Memora CLI interface.""" import typer +from memora.ai.conversational_ai import MemoraChat +from memora.ai.file_processor import FileProcessor +from memora.core.store import ObjectStore +from pathlib import Path app = typer.Typer(name="memora", help="Git-style versioned memory for LLMs") +@app.command() +def search(query: str): + """Search memory""" + store = ObjectStore(Path(".memora")) + + results = store.search(query) + if not results: + print("No results found") + else: + print("Results:") + for r in results: + print("-", r) + @app.command() def version(): """Show version information.""" @@ -17,5 +34,31 @@ def init(): print("Memora repository initialized") +@app.command() +def chat(): + """Start AI chat with memory.""" + bot = MemoraChat() + + print("Start chatting (type exit to quit)") + + while True: + msg = input("You: ") + if msg.lower() == "exit": + break + + response = bot.chat(msg) + print("AI:", response) + +@app.command() +def ingest(path: str): + """Ingest a file and extract memory""" + processor = FileProcessor() + + try: + count = processor.process_file(path) + print(f"Processed {count} facts from file") + except Exception as e: + print(f"Error: {e}") + if __name__ == "__main__": - app() + app() \ No newline at end of file diff --git a/test.txt b/test.txt new file mode 100644 index 0000000..3cff0b9 --- /dev/null +++ b/test.txt @@ -0,0 +1,2 @@ +My name is Nitheesh +I like Python \ No newline at end of file