basic-memory 0.12.2__py3-none-any.whl → 0.13.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.
Potentially problematic release.
This version of basic-memory might be problematic. Click here for more details.
- basic_memory/__init__.py +2 -1
- basic_memory/alembic/env.py +1 -1
- basic_memory/alembic/versions/5fe1ab1ccebe_add_projects_table.py +108 -0
- basic_memory/alembic/versions/647e7a75e2cd_project_constraint_fix.py +104 -0
- basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +0 -6
- basic_memory/api/app.py +43 -13
- basic_memory/api/routers/__init__.py +4 -2
- basic_memory/api/routers/directory_router.py +63 -0
- basic_memory/api/routers/importer_router.py +152 -0
- basic_memory/api/routers/knowledge_router.py +139 -37
- basic_memory/api/routers/management_router.py +78 -0
- basic_memory/api/routers/memory_router.py +6 -62
- basic_memory/api/routers/project_router.py +234 -0
- basic_memory/api/routers/prompt_router.py +260 -0
- basic_memory/api/routers/search_router.py +3 -21
- basic_memory/api/routers/utils.py +130 -0
- basic_memory/api/template_loader.py +292 -0
- basic_memory/cli/app.py +20 -21
- basic_memory/cli/commands/__init__.py +2 -1
- basic_memory/cli/commands/auth.py +136 -0
- basic_memory/cli/commands/db.py +3 -3
- basic_memory/cli/commands/import_chatgpt.py +31 -207
- basic_memory/cli/commands/import_claude_conversations.py +16 -142
- basic_memory/cli/commands/import_claude_projects.py +33 -143
- basic_memory/cli/commands/import_memory_json.py +26 -83
- basic_memory/cli/commands/mcp.py +71 -18
- basic_memory/cli/commands/project.py +102 -70
- basic_memory/cli/commands/status.py +19 -9
- basic_memory/cli/commands/sync.py +44 -58
- basic_memory/cli/commands/tool.py +6 -6
- basic_memory/cli/main.py +1 -5
- basic_memory/config.py +143 -87
- basic_memory/db.py +6 -4
- basic_memory/deps.py +227 -30
- basic_memory/importers/__init__.py +27 -0
- basic_memory/importers/base.py +79 -0
- basic_memory/importers/chatgpt_importer.py +222 -0
- basic_memory/importers/claude_conversations_importer.py +172 -0
- basic_memory/importers/claude_projects_importer.py +148 -0
- basic_memory/importers/memory_json_importer.py +93 -0
- basic_memory/importers/utils.py +58 -0
- basic_memory/markdown/entity_parser.py +5 -2
- basic_memory/mcp/auth_provider.py +270 -0
- basic_memory/mcp/external_auth_provider.py +321 -0
- basic_memory/mcp/project_session.py +103 -0
- basic_memory/mcp/prompts/__init__.py +2 -0
- basic_memory/mcp/prompts/continue_conversation.py +18 -68
- basic_memory/mcp/prompts/recent_activity.py +20 -4
- basic_memory/mcp/prompts/search.py +14 -140
- basic_memory/mcp/prompts/sync_status.py +116 -0
- basic_memory/mcp/prompts/utils.py +3 -3
- basic_memory/mcp/{tools → resources}/project_info.py +6 -2
- basic_memory/mcp/server.py +86 -13
- basic_memory/mcp/supabase_auth_provider.py +463 -0
- basic_memory/mcp/tools/__init__.py +24 -0
- basic_memory/mcp/tools/build_context.py +43 -8
- basic_memory/mcp/tools/canvas.py +17 -3
- basic_memory/mcp/tools/delete_note.py +168 -5
- basic_memory/mcp/tools/edit_note.py +303 -0
- basic_memory/mcp/tools/list_directory.py +154 -0
- basic_memory/mcp/tools/move_note.py +299 -0
- basic_memory/mcp/tools/project_management.py +332 -0
- basic_memory/mcp/tools/read_content.py +15 -6
- basic_memory/mcp/tools/read_note.py +28 -9
- basic_memory/mcp/tools/recent_activity.py +47 -16
- basic_memory/mcp/tools/search.py +189 -8
- basic_memory/mcp/tools/sync_status.py +254 -0
- basic_memory/mcp/tools/utils.py +184 -12
- basic_memory/mcp/tools/view_note.py +66 -0
- basic_memory/mcp/tools/write_note.py +24 -17
- basic_memory/models/__init__.py +3 -2
- basic_memory/models/knowledge.py +16 -4
- basic_memory/models/project.py +78 -0
- basic_memory/models/search.py +8 -5
- basic_memory/repository/__init__.py +2 -0
- basic_memory/repository/entity_repository.py +8 -3
- basic_memory/repository/observation_repository.py +35 -3
- basic_memory/repository/project_info_repository.py +3 -2
- basic_memory/repository/project_repository.py +85 -0
- basic_memory/repository/relation_repository.py +8 -2
- basic_memory/repository/repository.py +107 -15
- basic_memory/repository/search_repository.py +192 -54
- basic_memory/schemas/__init__.py +6 -0
- basic_memory/schemas/base.py +33 -5
- basic_memory/schemas/directory.py +30 -0
- basic_memory/schemas/importer.py +34 -0
- basic_memory/schemas/memory.py +84 -13
- basic_memory/schemas/project_info.py +112 -2
- basic_memory/schemas/prompt.py +90 -0
- basic_memory/schemas/request.py +56 -2
- basic_memory/schemas/search.py +1 -1
- basic_memory/services/__init__.py +2 -1
- basic_memory/services/context_service.py +208 -95
- basic_memory/services/directory_service.py +167 -0
- basic_memory/services/entity_service.py +399 -6
- basic_memory/services/exceptions.py +6 -0
- basic_memory/services/file_service.py +14 -15
- basic_memory/services/initialization.py +170 -66
- basic_memory/services/link_resolver.py +35 -12
- basic_memory/services/migration_service.py +168 -0
- basic_memory/services/project_service.py +671 -0
- basic_memory/services/search_service.py +77 -2
- basic_memory/services/sync_status_service.py +181 -0
- basic_memory/sync/background_sync.py +25 -0
- basic_memory/sync/sync_service.py +102 -21
- basic_memory/sync/watch_service.py +63 -39
- basic_memory/templates/prompts/continue_conversation.hbs +110 -0
- basic_memory/templates/prompts/search.hbs +101 -0
- basic_memory/utils.py +67 -17
- {basic_memory-0.12.2.dist-info → basic_memory-0.13.0.dist-info}/METADATA +26 -4
- basic_memory-0.13.0.dist-info/RECORD +138 -0
- basic_memory/api/routers/project_info_router.py +0 -274
- basic_memory/mcp/main.py +0 -24
- basic_memory-0.12.2.dist-info/RECORD +0 -100
- {basic_memory-0.12.2.dist-info → basic_memory-0.13.0.dist-info}/WHEEL +0 -0
- {basic_memory-0.12.2.dist-info → basic_memory-0.13.0.dist-info}/entry_points.txt +0 -0
- {basic_memory-0.12.2.dist-info → basic_memory-0.13.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
"""Write note tool for Basic Memory MCP server."""
|
|
2
2
|
|
|
3
|
-
from typing import List, Union
|
|
3
|
+
from typing import List, Union, Optional
|
|
4
4
|
|
|
5
5
|
from loguru import logger
|
|
6
6
|
|
|
7
7
|
from basic_memory.mcp.async_client import client
|
|
8
8
|
from basic_memory.mcp.server import mcp
|
|
9
9
|
from basic_memory.mcp.tools.utils import call_put
|
|
10
|
+
from basic_memory.mcp.project_session import get_active_project
|
|
10
11
|
from basic_memory.schemas import EntityResponse
|
|
11
12
|
from basic_memory.schemas.base import Entity
|
|
12
13
|
from basic_memory.utils import parse_tags
|
|
@@ -26,6 +27,7 @@ async def write_note(
|
|
|
26
27
|
content: str,
|
|
27
28
|
folder: str,
|
|
28
29
|
tags=None, # Remove type hint completely to avoid schema issues
|
|
30
|
+
project: Optional[str] = None,
|
|
29
31
|
) -> str:
|
|
30
32
|
"""Write a markdown note to the knowledge base.
|
|
31
33
|
|
|
@@ -52,9 +54,11 @@ async def write_note(
|
|
|
52
54
|
Args:
|
|
53
55
|
title: The title of the note
|
|
54
56
|
content: Markdown content for the note, can include observations and relations
|
|
55
|
-
folder:
|
|
57
|
+
folder: Folder path relative to project root where the file should be saved.
|
|
58
|
+
Use forward slashes (/) as separators. Examples: "notes", "projects/2025", "research/ml"
|
|
56
59
|
tags: Tags to categorize the note. Can be a list of strings, a comma-separated string, or None.
|
|
57
60
|
Note: If passing from external MCP clients, use a string format (e.g. "tag1,tag2,tag3")
|
|
61
|
+
project: Optional project name to write to. If not provided, uses current active project.
|
|
58
62
|
|
|
59
63
|
Returns:
|
|
60
64
|
A markdown formatted summary of the semantic content, including:
|
|
@@ -64,12 +68,19 @@ async def write_note(
|
|
|
64
68
|
- Relation counts (resolved/unresolved)
|
|
65
69
|
- Tags if present
|
|
66
70
|
"""
|
|
67
|
-
logger.info("MCP tool call
|
|
71
|
+
logger.info(f"MCP tool call tool=write_note folder={folder}, title={title}, tags={tags}")
|
|
72
|
+
|
|
73
|
+
# Check migration status and wait briefly if needed
|
|
74
|
+
from basic_memory.mcp.tools.utils import wait_for_migration_or_return_status
|
|
75
|
+
|
|
76
|
+
migration_status = await wait_for_migration_or_return_status(timeout=5.0)
|
|
77
|
+
if migration_status: # pragma: no cover
|
|
78
|
+
return f"# System Status\n\n{migration_status}\n\nPlease wait for migration to complete before creating notes."
|
|
68
79
|
|
|
69
80
|
# Process tags using the helper function
|
|
70
81
|
tag_list = parse_tags(tags)
|
|
71
82
|
# Create the entity request
|
|
72
|
-
metadata = {"tags":
|
|
83
|
+
metadata = {"tags": tag_list} if tag_list else None
|
|
73
84
|
entity = Entity(
|
|
74
85
|
title=title,
|
|
75
86
|
folder=folder,
|
|
@@ -78,10 +89,12 @@ async def write_note(
|
|
|
78
89
|
content=content,
|
|
79
90
|
entity_metadata=metadata,
|
|
80
91
|
)
|
|
92
|
+
active_project = get_active_project(project)
|
|
93
|
+
project_url = active_project.project_url
|
|
81
94
|
|
|
82
95
|
# Create or update via knowledge API
|
|
83
|
-
logger.debug("Creating entity via API
|
|
84
|
-
url = f"/knowledge/entities/{entity.permalink}"
|
|
96
|
+
logger.debug(f"Creating entity via API permalink={entity.permalink}")
|
|
97
|
+
url = f"{project_url}/knowledge/entities/{entity.permalink}"
|
|
85
98
|
response = await call_put(client, url, json=entity.model_dump())
|
|
86
99
|
result = EntityResponse.model_validate(response.json())
|
|
87
100
|
|
|
@@ -115,22 +128,16 @@ async def write_note(
|
|
|
115
128
|
summary.append(f"- Resolved: {resolved}")
|
|
116
129
|
if unresolved:
|
|
117
130
|
summary.append(f"- Unresolved: {unresolved}")
|
|
118
|
-
summary.append("\
|
|
131
|
+
summary.append("\nNote: Unresolved relations point to entities that don't exist yet.")
|
|
132
|
+
summary.append(
|
|
133
|
+
"They will be automatically resolved when target entities are created or during sync operations."
|
|
134
|
+
)
|
|
119
135
|
|
|
120
136
|
if tag_list:
|
|
121
137
|
summary.append(f"\n## Tags\n- {', '.join(tag_list)}")
|
|
122
138
|
|
|
123
139
|
# Log the response with structured data
|
|
124
140
|
logger.info(
|
|
125
|
-
"MCP tool response"
|
|
126
|
-
tool="write_note",
|
|
127
|
-
action=action,
|
|
128
|
-
permalink=result.permalink,
|
|
129
|
-
observations_count=len(result.observations),
|
|
130
|
-
relations_count=len(result.relations),
|
|
131
|
-
resolved_relations=resolved,
|
|
132
|
-
unresolved_relations=unresolved,
|
|
133
|
-
status_code=response.status_code,
|
|
141
|
+
f"MCP tool response: tool=write_note action={action} permalink={result.permalink} observations_count={len(result.observations)} relations_count={len(result.relations)} resolved_relations={resolved} unresolved_relations={unresolved} status_code={response.status_code}"
|
|
134
142
|
)
|
|
135
|
-
|
|
136
143
|
return "\n".join(summary)
|
basic_memory/models/__init__.py
CHANGED
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
import basic_memory
|
|
4
4
|
from basic_memory.models.base import Base
|
|
5
5
|
from basic_memory.models.knowledge import Entity, Observation, Relation
|
|
6
|
-
|
|
7
|
-
SCHEMA_VERSION = basic_memory.__version__ + "-" + "003"
|
|
6
|
+
from basic_memory.models.project import Project
|
|
8
7
|
|
|
9
8
|
__all__ = [
|
|
10
9
|
"Base",
|
|
11
10
|
"Entity",
|
|
12
11
|
"Observation",
|
|
13
12
|
"Relation",
|
|
13
|
+
"Project",
|
|
14
|
+
"basic_memory",
|
|
14
15
|
]
|
basic_memory/models/knowledge.py
CHANGED
|
@@ -17,7 +17,6 @@ from sqlalchemy import (
|
|
|
17
17
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
18
18
|
|
|
19
19
|
from basic_memory.models.base import Base
|
|
20
|
-
|
|
21
20
|
from basic_memory.utils import generate_permalink
|
|
22
21
|
|
|
23
22
|
|
|
@@ -29,6 +28,7 @@ class Entity(Base):
|
|
|
29
28
|
- Maps to a file on disk
|
|
30
29
|
- Maintains a checksum for change detection
|
|
31
30
|
- Tracks both source file and semantic properties
|
|
31
|
+
- Belongs to a specific project
|
|
32
32
|
"""
|
|
33
33
|
|
|
34
34
|
__tablename__ = "entity"
|
|
@@ -38,13 +38,21 @@ class Entity(Base):
|
|
|
38
38
|
Index("ix_entity_title", "title"),
|
|
39
39
|
Index("ix_entity_created_at", "created_at"), # For timeline queries
|
|
40
40
|
Index("ix_entity_updated_at", "updated_at"), # For timeline queries
|
|
41
|
-
#
|
|
41
|
+
Index("ix_entity_project_id", "project_id"), # For project filtering
|
|
42
|
+
# Project-specific uniqueness constraints
|
|
42
43
|
Index(
|
|
43
|
-
"
|
|
44
|
+
"uix_entity_permalink_project",
|
|
44
45
|
"permalink",
|
|
46
|
+
"project_id",
|
|
45
47
|
unique=True,
|
|
46
48
|
sqlite_where=text("content_type = 'text/markdown' AND permalink IS NOT NULL"),
|
|
47
49
|
),
|
|
50
|
+
Index(
|
|
51
|
+
"uix_entity_file_path_project",
|
|
52
|
+
"file_path",
|
|
53
|
+
"project_id",
|
|
54
|
+
unique=True,
|
|
55
|
+
),
|
|
48
56
|
)
|
|
49
57
|
|
|
50
58
|
# Core identity
|
|
@@ -54,10 +62,13 @@ class Entity(Base):
|
|
|
54
62
|
entity_metadata: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True)
|
|
55
63
|
content_type: Mapped[str] = mapped_column(String)
|
|
56
64
|
|
|
65
|
+
# Project reference
|
|
66
|
+
project_id: Mapped[int] = mapped_column(Integer, ForeignKey("project.id"), nullable=False)
|
|
67
|
+
|
|
57
68
|
# Normalized path for URIs - required for markdown files only
|
|
58
69
|
permalink: Mapped[Optional[str]] = mapped_column(String, nullable=True, index=True)
|
|
59
70
|
# Actual filesystem relative path
|
|
60
|
-
file_path: Mapped[str] = mapped_column(String,
|
|
71
|
+
file_path: Mapped[str] = mapped_column(String, index=True)
|
|
61
72
|
# checksum of file
|
|
62
73
|
checksum: Mapped[Optional[str]] = mapped_column(String, nullable=True)
|
|
63
74
|
|
|
@@ -66,6 +77,7 @@ class Entity(Base):
|
|
|
66
77
|
updated_at: Mapped[datetime] = mapped_column(DateTime)
|
|
67
78
|
|
|
68
79
|
# Relationships
|
|
80
|
+
project = relationship("Project", back_populates="entities")
|
|
69
81
|
observations = relationship(
|
|
70
82
|
"Observation", back_populates="entity", cascade="all, delete-orphan"
|
|
71
83
|
)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Project model for Basic Memory."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from sqlalchemy import (
|
|
7
|
+
Integer,
|
|
8
|
+
String,
|
|
9
|
+
Text,
|
|
10
|
+
Boolean,
|
|
11
|
+
DateTime,
|
|
12
|
+
Index,
|
|
13
|
+
event,
|
|
14
|
+
)
|
|
15
|
+
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
16
|
+
|
|
17
|
+
from basic_memory.models.base import Base
|
|
18
|
+
from basic_memory.utils import generate_permalink
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Project(Base):
|
|
22
|
+
"""Project model for Basic Memory.
|
|
23
|
+
|
|
24
|
+
A project represents a collection of knowledge entities that are grouped together.
|
|
25
|
+
Projects are stored in the app-level database and provide context for all knowledge
|
|
26
|
+
operations.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
__tablename__ = "project"
|
|
30
|
+
__table_args__ = (
|
|
31
|
+
# Regular indexes
|
|
32
|
+
Index("ix_project_name", "name", unique=True),
|
|
33
|
+
Index("ix_project_permalink", "permalink", unique=True),
|
|
34
|
+
Index("ix_project_path", "path"),
|
|
35
|
+
Index("ix_project_created_at", "created_at"),
|
|
36
|
+
Index("ix_project_updated_at", "updated_at"),
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# Core identity
|
|
40
|
+
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
41
|
+
name: Mapped[str] = mapped_column(String, unique=True)
|
|
42
|
+
description: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
|
43
|
+
|
|
44
|
+
# URL-friendly identifier generated from name
|
|
45
|
+
permalink: Mapped[str] = mapped_column(String, unique=True)
|
|
46
|
+
|
|
47
|
+
# Filesystem path to project directory
|
|
48
|
+
path: Mapped[str] = mapped_column(String)
|
|
49
|
+
|
|
50
|
+
# Status flags
|
|
51
|
+
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
|
52
|
+
is_default: Mapped[Optional[bool]] = mapped_column(Boolean, default=None, nullable=True)
|
|
53
|
+
|
|
54
|
+
# Timestamps
|
|
55
|
+
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
|
56
|
+
updated_at: Mapped[datetime] = mapped_column(
|
|
57
|
+
DateTime, default=datetime.utcnow, onupdate=datetime.utcnow
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Define relationships to entities, observations, and relations
|
|
61
|
+
# These relationships will be established once we add project_id to those models
|
|
62
|
+
entities = relationship("Entity", back_populates="project", cascade="all, delete-orphan")
|
|
63
|
+
|
|
64
|
+
def __repr__(self) -> str: # pragma: no cover
|
|
65
|
+
return f"Project(id={self.id}, name='{self.name}', permalink='{self.permalink}', path='{self.path}')"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@event.listens_for(Project, "before_insert")
|
|
69
|
+
@event.listens_for(Project, "before_update")
|
|
70
|
+
def set_project_permalink(mapper, connection, project):
|
|
71
|
+
"""Generate URL-friendly permalink for the project if needed.
|
|
72
|
+
|
|
73
|
+
This event listener ensures the permalink is always derived from the name,
|
|
74
|
+
even if the name changes.
|
|
75
|
+
"""
|
|
76
|
+
# If the name changed or permalink is empty, regenerate permalink
|
|
77
|
+
if not project.permalink or project.permalink != generate_permalink(project.name):
|
|
78
|
+
project.permalink = generate_permalink(project.name)
|
basic_memory/models/search.py
CHANGED
|
@@ -13,21 +13,24 @@ CREATE VIRTUAL TABLE IF NOT EXISTS search_index USING fts5(
|
|
|
13
13
|
permalink, -- Stable identifier (now indexed for path search)
|
|
14
14
|
file_path UNINDEXED, -- Physical location
|
|
15
15
|
type UNINDEXED, -- entity/relation/observation
|
|
16
|
-
|
|
17
|
-
--
|
|
16
|
+
|
|
17
|
+
-- Project context
|
|
18
|
+
project_id UNINDEXED, -- Project identifier
|
|
19
|
+
|
|
20
|
+
-- Relation fields
|
|
18
21
|
from_id UNINDEXED, -- Source entity
|
|
19
22
|
to_id UNINDEXED, -- Target entity
|
|
20
23
|
relation_type UNINDEXED, -- Type of relation
|
|
21
|
-
|
|
24
|
+
|
|
22
25
|
-- Observation fields
|
|
23
26
|
entity_id UNINDEXED, -- Parent entity
|
|
24
27
|
category UNINDEXED, -- Observation category
|
|
25
|
-
|
|
28
|
+
|
|
26
29
|
-- Common fields
|
|
27
30
|
metadata UNINDEXED, -- JSON metadata
|
|
28
31
|
created_at UNINDEXED, -- Creation timestamp
|
|
29
32
|
updated_at UNINDEXED, -- Last update
|
|
30
|
-
|
|
33
|
+
|
|
31
34
|
-- Configuration
|
|
32
35
|
tokenize='unicode61 tokenchars 0x2F', -- Hex code for /
|
|
33
36
|
prefix='1,2,3,4' -- Support longer prefixes for paths
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
from .entity_repository import EntityRepository
|
|
2
2
|
from .observation_repository import ObservationRepository
|
|
3
|
+
from .project_repository import ProjectRepository
|
|
3
4
|
from .relation_repository import RelationRepository
|
|
4
5
|
|
|
5
6
|
__all__ = [
|
|
6
7
|
"EntityRepository",
|
|
7
8
|
"ObservationRepository",
|
|
9
|
+
"ProjectRepository",
|
|
8
10
|
"RelationRepository",
|
|
9
11
|
]
|
|
@@ -18,9 +18,14 @@ class EntityRepository(Repository[Entity]):
|
|
|
18
18
|
to strings before passing to repository methods.
|
|
19
19
|
"""
|
|
20
20
|
|
|
21
|
-
def __init__(self, session_maker: async_sessionmaker[AsyncSession]):
|
|
22
|
-
"""Initialize with session maker.
|
|
23
|
-
|
|
21
|
+
def __init__(self, session_maker: async_sessionmaker[AsyncSession], project_id: int):
|
|
22
|
+
"""Initialize with session maker and project_id filter.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
session_maker: SQLAlchemy session maker
|
|
26
|
+
project_id: Project ID to filter all operations by
|
|
27
|
+
"""
|
|
28
|
+
super().__init__(session_maker, Entity, project_id=project_id)
|
|
24
29
|
|
|
25
30
|
async def get_by_permalink(self, permalink: str) -> Optional[Entity]:
|
|
26
31
|
"""Get entity by permalink.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Repository for managing Observation objects."""
|
|
2
2
|
|
|
3
|
-
from typing import Sequence
|
|
3
|
+
from typing import Dict, List, Sequence
|
|
4
4
|
|
|
5
5
|
from sqlalchemy import select
|
|
6
6
|
from sqlalchemy.ext.asyncio import async_sessionmaker
|
|
@@ -12,8 +12,14 @@ from basic_memory.repository.repository import Repository
|
|
|
12
12
|
class ObservationRepository(Repository[Observation]):
|
|
13
13
|
"""Repository for Observation model with memory-specific operations."""
|
|
14
14
|
|
|
15
|
-
def __init__(self, session_maker: async_sessionmaker):
|
|
16
|
-
|
|
15
|
+
def __init__(self, session_maker: async_sessionmaker, project_id: int):
|
|
16
|
+
"""Initialize with session maker and project_id filter.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
session_maker: SQLAlchemy session maker
|
|
20
|
+
project_id: Project ID to filter all operations by
|
|
21
|
+
"""
|
|
22
|
+
super().__init__(session_maker, Observation, project_id=project_id)
|
|
17
23
|
|
|
18
24
|
async def find_by_entity(self, entity_id: int) -> Sequence[Observation]:
|
|
19
25
|
"""Find all observations for a specific entity."""
|
|
@@ -38,3 +44,29 @@ class ObservationRepository(Repository[Observation]):
|
|
|
38
44
|
query = select(Observation.category).distinct()
|
|
39
45
|
result = await self.execute_query(query, use_query_options=False)
|
|
40
46
|
return result.scalars().all()
|
|
47
|
+
|
|
48
|
+
async def find_by_entities(self, entity_ids: List[int]) -> Dict[int, List[Observation]]:
|
|
49
|
+
"""Find all observations for multiple entities in a single query.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
entity_ids: List of entity IDs to fetch observations for
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Dictionary mapping entity_id to list of observations
|
|
56
|
+
"""
|
|
57
|
+
if not entity_ids: # pragma: no cover
|
|
58
|
+
return {}
|
|
59
|
+
|
|
60
|
+
# Query observations for all entities in the list
|
|
61
|
+
query = select(Observation).filter(Observation.entity_id.in_(entity_ids))
|
|
62
|
+
result = await self.execute_query(query)
|
|
63
|
+
observations = result.scalars().all()
|
|
64
|
+
|
|
65
|
+
# Group observations by entity_id
|
|
66
|
+
observations_by_entity = {}
|
|
67
|
+
for obs in observations:
|
|
68
|
+
if obs.entity_id not in observations_by_entity:
|
|
69
|
+
observations_by_entity[obs.entity_id] = []
|
|
70
|
+
observations_by_entity[obs.entity_id].append(obs)
|
|
71
|
+
|
|
72
|
+
return observations_by_entity
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
from basic_memory.repository.repository import Repository
|
|
2
|
+
from basic_memory.models.project import Project
|
|
2
3
|
|
|
3
4
|
|
|
4
5
|
class ProjectInfoRepository(Repository):
|
|
5
6
|
"""Repository for statistics queries."""
|
|
6
7
|
|
|
7
8
|
def __init__(self, session_maker):
|
|
8
|
-
# Initialize with
|
|
9
|
-
super().__init__(session_maker,
|
|
9
|
+
# Initialize with Project model as a reference
|
|
10
|
+
super().__init__(session_maker, Project)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Repository for managing projects in Basic Memory."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional, Sequence, Union
|
|
5
|
+
|
|
6
|
+
from sqlalchemy import text
|
|
7
|
+
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
|
8
|
+
|
|
9
|
+
from basic_memory import db
|
|
10
|
+
from basic_memory.models.project import Project
|
|
11
|
+
from basic_memory.repository.repository import Repository
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ProjectRepository(Repository[Project]):
|
|
15
|
+
"""Repository for Project model.
|
|
16
|
+
|
|
17
|
+
Projects represent collections of knowledge entities grouped together.
|
|
18
|
+
Each entity, observation, and relation belongs to a specific project.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, session_maker: async_sessionmaker[AsyncSession]):
|
|
22
|
+
"""Initialize with session maker."""
|
|
23
|
+
super().__init__(session_maker, Project)
|
|
24
|
+
|
|
25
|
+
async def get_by_name(self, name: str) -> Optional[Project]:
|
|
26
|
+
"""Get project by name.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
name: Unique name of the project
|
|
30
|
+
"""
|
|
31
|
+
query = self.select().where(Project.name == name)
|
|
32
|
+
return await self.find_one(query)
|
|
33
|
+
|
|
34
|
+
async def get_by_permalink(self, permalink: str) -> Optional[Project]:
|
|
35
|
+
"""Get project by permalink.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
permalink: URL-friendly identifier for the project
|
|
39
|
+
"""
|
|
40
|
+
query = self.select().where(Project.permalink == permalink)
|
|
41
|
+
return await self.find_one(query)
|
|
42
|
+
|
|
43
|
+
async def get_by_path(self, path: Union[Path, str]) -> Optional[Project]:
|
|
44
|
+
"""Get project by filesystem path.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
path: Path to the project directory (will be converted to string internally)
|
|
48
|
+
"""
|
|
49
|
+
query = self.select().where(Project.path == str(path))
|
|
50
|
+
return await self.find_one(query)
|
|
51
|
+
|
|
52
|
+
async def get_default_project(self) -> Optional[Project]:
|
|
53
|
+
"""Get the default project (the one marked as is_default=True)."""
|
|
54
|
+
query = self.select().where(Project.is_default.is_not(None))
|
|
55
|
+
return await self.find_one(query)
|
|
56
|
+
|
|
57
|
+
async def get_active_projects(self) -> Sequence[Project]:
|
|
58
|
+
"""Get all active projects."""
|
|
59
|
+
query = self.select().where(Project.is_active == True) # noqa: E712
|
|
60
|
+
result = await self.execute_query(query)
|
|
61
|
+
return list(result.scalars().all())
|
|
62
|
+
|
|
63
|
+
async def set_as_default(self, project_id: int) -> Optional[Project]:
|
|
64
|
+
"""Set a project as the default and unset previous default.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
project_id: ID of the project to set as default
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
The updated project if found, None otherwise
|
|
71
|
+
"""
|
|
72
|
+
async with db.scoped_session(self.session_maker) as session:
|
|
73
|
+
# First, clear the default flag for all projects using direct SQL
|
|
74
|
+
await session.execute(
|
|
75
|
+
text("UPDATE project SET is_default = NULL WHERE is_default IS NOT NULL")
|
|
76
|
+
)
|
|
77
|
+
await session.flush()
|
|
78
|
+
|
|
79
|
+
# Set the new default project
|
|
80
|
+
target_project = await self.select_by_id(session, project_id)
|
|
81
|
+
if target_project:
|
|
82
|
+
target_project.is_default = True
|
|
83
|
+
await session.flush()
|
|
84
|
+
return target_project
|
|
85
|
+
return None # pragma: no cover
|
|
@@ -16,8 +16,14 @@ from basic_memory.repository.repository import Repository
|
|
|
16
16
|
class RelationRepository(Repository[Relation]):
|
|
17
17
|
"""Repository for Relation model with memory-specific operations."""
|
|
18
18
|
|
|
19
|
-
def __init__(self, session_maker: async_sessionmaker):
|
|
20
|
-
|
|
19
|
+
def __init__(self, session_maker: async_sessionmaker, project_id: int):
|
|
20
|
+
"""Initialize with session maker and project_id filter.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
session_maker: SQLAlchemy session maker
|
|
24
|
+
project_id: Project ID to filter all operations by
|
|
25
|
+
"""
|
|
26
|
+
super().__init__(session_maker, Relation, project_id=project_id)
|
|
21
27
|
|
|
22
28
|
async def find_relation(
|
|
23
29
|
self, from_permalink: str, to_permalink: str, relation_type: str
|