notes memory
This commit is contained in:
398
memory.py
Normal file
398
memory.py
Normal file
@@ -0,0 +1,398 @@
|
||||
import os
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
|
||||
|
||||
def get_memory_db():
|
||||
return Path(os.getenv("MEMORY_DB", ROOT / "data" / "memory.sqlite"))
|
||||
|
||||
|
||||
def connect():
|
||||
memory_db = get_memory_db()
|
||||
memory_db.parent.mkdir(parents=True, exist_ok=True)
|
||||
connection = sqlite3.connect(memory_db)
|
||||
connection.row_factory = sqlite3.Row
|
||||
connection.execute("PRAGMA foreign_keys = ON")
|
||||
return connection
|
||||
|
||||
|
||||
def init_db():
|
||||
with connect() as db:
|
||||
db.executescript(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS clients (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE COLLATE NOCASE,
|
||||
status TEXT DEFAULT 'actif',
|
||||
notes TEXT DEFAULT '',
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS projects (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
client_id INTEGER REFERENCES clients(id) ON DELETE SET NULL,
|
||||
name TEXT NOT NULL UNIQUE COLLATE NOCASE,
|
||||
status TEXT DEFAULT 'actif',
|
||||
summary TEXT DEFAULT '',
|
||||
next_action TEXT DEFAULT '',
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tasks (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
client_id INTEGER REFERENCES clients(id) ON DELETE SET NULL,
|
||||
project_id INTEGER REFERENCES projects(id) ON DELETE SET NULL,
|
||||
title TEXT NOT NULL,
|
||||
details TEXT DEFAULT '',
|
||||
due_at TEXT DEFAULT '',
|
||||
done INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS repositories (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
project_id INTEGER REFERENCES projects(id) ON DELETE SET NULL,
|
||||
name TEXT NOT NULL,
|
||||
local_path TEXT DEFAULT '',
|
||||
remote_url TEXT DEFAULT '',
|
||||
main_branch TEXT DEFAULT '',
|
||||
last_indexed_at TEXT DEFAULT '',
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(name, local_path, remote_url)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS memories (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
client_id INTEGER REFERENCES clients(id) ON DELETE SET NULL,
|
||||
project_id INTEGER REFERENCES projects(id) ON DELETE SET NULL,
|
||||
kind TEXT NOT NULL DEFAULT 'note',
|
||||
subject TEXT DEFAULT '',
|
||||
content TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def row_to_dict(row):
|
||||
return dict(row) if row else None
|
||||
|
||||
|
||||
def find_client(db, name):
|
||||
if not name:
|
||||
return None
|
||||
return db.execute(
|
||||
"SELECT * FROM clients WHERE name = ? COLLATE NOCASE",
|
||||
(clean_text(name),),
|
||||
).fetchone()
|
||||
|
||||
|
||||
def find_project(db, name):
|
||||
if not name:
|
||||
return None
|
||||
return db.execute(
|
||||
"SELECT * FROM projects WHERE name = ? COLLATE NOCASE",
|
||||
(clean_text(name),),
|
||||
).fetchone()
|
||||
|
||||
|
||||
def upsert_client(name, notes="", status="actif"):
|
||||
name = clean_text(name)
|
||||
notes = clean_text(notes)
|
||||
status = clean_text(status) or "actif"
|
||||
if not name:
|
||||
raise ValueError("Nom de client manquant")
|
||||
|
||||
with connect() as db:
|
||||
existing = find_client(db, name)
|
||||
if existing:
|
||||
merged_notes = merge_notes(existing["notes"], notes)
|
||||
db.execute(
|
||||
"""
|
||||
UPDATE clients
|
||||
SET status = ?, notes = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
""",
|
||||
(status, merged_notes, existing["id"]),
|
||||
)
|
||||
client_id = existing["id"]
|
||||
else:
|
||||
cursor = db.execute(
|
||||
"INSERT INTO clients (name, status, notes) VALUES (?, ?, ?)",
|
||||
(name, status, notes),
|
||||
)
|
||||
client_id = cursor.lastrowid
|
||||
return row_to_dict(db.execute("SELECT * FROM clients WHERE id = ?", (client_id,)).fetchone())
|
||||
|
||||
|
||||
def upsert_project(name, client_name="", status="actif", summary="", next_action=""):
|
||||
name = clean_text(name)
|
||||
if not name:
|
||||
raise ValueError("Nom de projet manquant")
|
||||
|
||||
client_id = None
|
||||
with connect() as db:
|
||||
if client_name:
|
||||
client = find_client(db, client_name)
|
||||
if client is None:
|
||||
cursor = db.execute(
|
||||
"INSERT INTO clients (name, notes) VALUES (?, ?)",
|
||||
(clean_text(client_name), "Ajoute automatiquement depuis un projet."),
|
||||
)
|
||||
client_id = cursor.lastrowid
|
||||
else:
|
||||
client_id = client["id"]
|
||||
|
||||
existing = find_project(db, name)
|
||||
if existing:
|
||||
db.execute(
|
||||
"""
|
||||
UPDATE projects
|
||||
SET client_id = COALESCE(?, client_id),
|
||||
status = COALESCE(NULLIF(?, ''), status),
|
||||
summary = COALESCE(NULLIF(?, ''), summary),
|
||||
next_action = COALESCE(NULLIF(?, ''), next_action),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
""",
|
||||
(client_id, clean_text(status), clean_text(summary), clean_text(next_action), existing["id"]),
|
||||
)
|
||||
project_id = existing["id"]
|
||||
else:
|
||||
cursor = db.execute(
|
||||
"""
|
||||
INSERT INTO projects (client_id, name, status, summary, next_action)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
""",
|
||||
(client_id, name, clean_text(status) or "actif", clean_text(summary), clean_text(next_action)),
|
||||
)
|
||||
project_id = cursor.lastrowid
|
||||
return row_to_dict(db.execute("SELECT * FROM projects WHERE id = ?", (project_id,)).fetchone())
|
||||
|
||||
|
||||
def add_task(title, client_name="", project_name="", details="", due_at=""):
|
||||
title = clean_text(title)
|
||||
if not title:
|
||||
raise ValueError("Titre de tache manquant")
|
||||
|
||||
with connect() as db:
|
||||
client_id = ensure_client_id(db, client_name)
|
||||
project_id = ensure_project_id(db, project_name, client_id)
|
||||
cursor = db.execute(
|
||||
"""
|
||||
INSERT INTO tasks (client_id, project_id, title, details, due_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
""",
|
||||
(client_id, project_id, title, clean_text(details), clean_text(due_at)),
|
||||
)
|
||||
return row_to_dict(db.execute("SELECT * FROM tasks WHERE id = ?", (cursor.lastrowid,)).fetchone())
|
||||
|
||||
|
||||
def add_repository(name, project_name="", local_path="", remote_url="", main_branch=""):
|
||||
name = clean_text(name) or derive_repo_name(local_path, remote_url)
|
||||
if not name:
|
||||
raise ValueError("Nom de depot manquant")
|
||||
|
||||
with connect() as db:
|
||||
project_id = ensure_project_id(db, project_name, None)
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO repositories (project_id, name, local_path, remote_url, main_branch)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
ON CONFLICT(name, local_path, remote_url) DO UPDATE SET
|
||||
project_id = COALESCE(excluded.project_id, repositories.project_id),
|
||||
main_branch = COALESCE(NULLIF(excluded.main_branch, ''), repositories.main_branch),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
""",
|
||||
(project_id, name, clean_text(local_path), clean_text(remote_url), clean_text(main_branch)),
|
||||
)
|
||||
return row_to_dict(
|
||||
db.execute(
|
||||
"""
|
||||
SELECT * FROM repositories
|
||||
WHERE name = ? AND local_path = ? AND remote_url = ?
|
||||
""",
|
||||
(name, clean_text(local_path), clean_text(remote_url)),
|
||||
).fetchone()
|
||||
)
|
||||
|
||||
|
||||
def add_memory(content, kind="note", subject="", client_name="", project_name=""):
|
||||
content = clean_text(content)
|
||||
if not content:
|
||||
raise ValueError("Memoire vide")
|
||||
|
||||
with connect() as db:
|
||||
client_id = ensure_client_id(db, client_name)
|
||||
project_id = ensure_project_id(db, project_name, client_id)
|
||||
cursor = db.execute(
|
||||
"""
|
||||
INSERT INTO memories (client_id, project_id, kind, subject, content)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
""",
|
||||
(client_id, project_id, clean_text(kind) or "note", clean_text(subject), content),
|
||||
)
|
||||
return row_to_dict(db.execute("SELECT * FROM memories WHERE id = ?", (cursor.lastrowid,)).fetchone())
|
||||
|
||||
|
||||
def search_memory(query="", limit=12):
|
||||
query = clean_text(query)
|
||||
like = f"%{query}%"
|
||||
with connect() as db:
|
||||
if query:
|
||||
clients = db.execute(
|
||||
"SELECT * FROM clients WHERE name LIKE ? OR notes LIKE ? ORDER BY updated_at DESC LIMIT ?",
|
||||
(like, like, limit),
|
||||
).fetchall()
|
||||
projects = db.execute(
|
||||
"""
|
||||
SELECT p.*, c.name AS client_name
|
||||
FROM projects p
|
||||
LEFT JOIN clients c ON c.id = p.client_id
|
||||
WHERE p.name LIKE ? OR p.summary LIKE ? OR p.next_action LIKE ? OR c.name LIKE ?
|
||||
ORDER BY p.updated_at DESC LIMIT ?
|
||||
""",
|
||||
(like, like, like, like, limit),
|
||||
).fetchall()
|
||||
tasks = db.execute(
|
||||
"""
|
||||
SELECT t.*, c.name AS client_name, p.name AS project_name
|
||||
FROM tasks t
|
||||
LEFT JOIN clients c ON c.id = t.client_id
|
||||
LEFT JOIN projects p ON p.id = t.project_id
|
||||
WHERE t.done = 0 AND (t.title LIKE ? OR t.details LIKE ? OR c.name LIKE ? OR p.name LIKE ?)
|
||||
ORDER BY t.created_at DESC LIMIT ?
|
||||
""",
|
||||
(like, like, like, like, limit),
|
||||
).fetchall()
|
||||
repos = db.execute(
|
||||
"""
|
||||
SELECT r.*, p.name AS project_name
|
||||
FROM repositories r
|
||||
LEFT JOIN projects p ON p.id = r.project_id
|
||||
WHERE r.name LIKE ? OR r.local_path LIKE ? OR r.remote_url LIKE ? OR p.name LIKE ?
|
||||
ORDER BY r.updated_at DESC LIMIT ?
|
||||
""",
|
||||
(like, like, like, like, limit),
|
||||
).fetchall()
|
||||
notes = db.execute(
|
||||
"""
|
||||
SELECT m.*, c.name AS client_name, p.name AS project_name
|
||||
FROM memories m
|
||||
LEFT JOIN clients c ON c.id = m.client_id
|
||||
LEFT JOIN projects p ON p.id = m.project_id
|
||||
WHERE m.subject LIKE ? OR m.content LIKE ? OR c.name LIKE ? OR p.name LIKE ?
|
||||
ORDER BY m.created_at DESC LIMIT ?
|
||||
""",
|
||||
(like, like, like, like, limit),
|
||||
).fetchall()
|
||||
else:
|
||||
clients = db.execute("SELECT * FROM clients ORDER BY updated_at DESC LIMIT ?", (limit,)).fetchall()
|
||||
projects = db.execute(
|
||||
"""
|
||||
SELECT p.*, c.name AS client_name
|
||||
FROM projects p
|
||||
LEFT JOIN clients c ON c.id = p.client_id
|
||||
ORDER BY p.updated_at DESC LIMIT ?
|
||||
""",
|
||||
(limit,),
|
||||
).fetchall()
|
||||
tasks = db.execute(
|
||||
"""
|
||||
SELECT t.*, c.name AS client_name, p.name AS project_name
|
||||
FROM tasks t
|
||||
LEFT JOIN clients c ON c.id = t.client_id
|
||||
LEFT JOIN projects p ON p.id = t.project_id
|
||||
WHERE t.done = 0
|
||||
ORDER BY t.created_at DESC LIMIT ?
|
||||
""",
|
||||
(limit,),
|
||||
).fetchall()
|
||||
repos = db.execute(
|
||||
"""
|
||||
SELECT r.*, p.name AS project_name
|
||||
FROM repositories r
|
||||
LEFT JOIN projects p ON p.id = r.project_id
|
||||
ORDER BY r.updated_at DESC LIMIT ?
|
||||
""",
|
||||
(limit,),
|
||||
).fetchall()
|
||||
notes = db.execute(
|
||||
"""
|
||||
SELECT m.*, c.name AS client_name, p.name AS project_name
|
||||
FROM memories m
|
||||
LEFT JOIN clients c ON c.id = m.client_id
|
||||
LEFT JOIN projects p ON p.id = m.project_id
|
||||
ORDER BY m.created_at DESC LIMIT ?
|
||||
""",
|
||||
(limit,),
|
||||
).fetchall()
|
||||
|
||||
return {
|
||||
"clients": [row_to_dict(row) for row in clients],
|
||||
"projects": [row_to_dict(row) for row in projects],
|
||||
"tasks": [row_to_dict(row) for row in tasks],
|
||||
"repositories": [row_to_dict(row) for row in repos],
|
||||
"memories": [row_to_dict(row) for row in notes],
|
||||
}
|
||||
|
||||
|
||||
def ensure_client_id(db, client_name):
|
||||
client_name = clean_text(client_name)
|
||||
if not client_name:
|
||||
return None
|
||||
client = find_client(db, client_name)
|
||||
if client:
|
||||
return client["id"]
|
||||
cursor = db.execute("INSERT INTO clients (name) VALUES (?)", (client_name,))
|
||||
return cursor.lastrowid
|
||||
|
||||
|
||||
def ensure_project_id(db, project_name, client_id):
|
||||
project_name = clean_text(project_name)
|
||||
if not project_name:
|
||||
return None
|
||||
project = find_project(db, project_name)
|
||||
if project:
|
||||
if client_id and not project["client_id"]:
|
||||
db.execute(
|
||||
"UPDATE projects SET client_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?",
|
||||
(client_id, project["id"]),
|
||||
)
|
||||
return project["id"]
|
||||
cursor = db.execute(
|
||||
"INSERT INTO projects (client_id, name) VALUES (?, ?)",
|
||||
(client_id, project_name),
|
||||
)
|
||||
return cursor.lastrowid
|
||||
|
||||
|
||||
def merge_notes(existing, new):
|
||||
existing = clean_text(existing)
|
||||
new = clean_text(new)
|
||||
if not new:
|
||||
return existing
|
||||
if not existing:
|
||||
return new
|
||||
if new.lower() in existing.lower():
|
||||
return existing
|
||||
return f"{existing}\n{new}"
|
||||
|
||||
|
||||
def clean_text(value):
|
||||
return " ".join(str(value or "").strip().split())
|
||||
|
||||
|
||||
def derive_repo_name(local_path, remote_url):
|
||||
source = clean_text(local_path) or clean_text(remote_url)
|
||||
if not source:
|
||||
return ""
|
||||
name = source.rstrip("/\\").split("/")[-1].split("\\")[-1]
|
||||
return name.removesuffix(".git")
|
||||
Reference in New Issue
Block a user