basic-memory 0.1.1__py3-none-any.whl → 0.1.2__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 +1 -1
- basic_memory/alembic/README +1 -0
- basic_memory/alembic/env.py +75 -0
- basic_memory/alembic/migrations.py +29 -0
- basic_memory/alembic/script.py.mako +26 -0
- basic_memory/alembic/versions/3dae7c7b1564_initial_schema.py +93 -0
- basic_memory/api/__init__.py +2 -1
- basic_memory/api/app.py +26 -24
- basic_memory/api/routers/knowledge_router.py +28 -26
- basic_memory/api/routers/memory_router.py +17 -11
- basic_memory/api/routers/search_router.py +6 -12
- basic_memory/cli/__init__.py +1 -1
- basic_memory/cli/app.py +0 -1
- basic_memory/cli/commands/__init__.py +3 -3
- basic_memory/cli/commands/db.py +25 -0
- basic_memory/cli/commands/import_memory_json.py +35 -31
- basic_memory/cli/commands/mcp.py +20 -0
- basic_memory/cli/commands/status.py +10 -6
- basic_memory/cli/commands/sync.py +5 -56
- basic_memory/cli/main.py +5 -38
- basic_memory/config.py +3 -3
- basic_memory/db.py +15 -22
- basic_memory/deps.py +3 -4
- basic_memory/file_utils.py +36 -35
- basic_memory/markdown/entity_parser.py +13 -30
- basic_memory/markdown/markdown_processor.py +7 -7
- basic_memory/markdown/plugins.py +109 -123
- basic_memory/markdown/schemas.py +7 -8
- basic_memory/markdown/utils.py +70 -121
- basic_memory/mcp/__init__.py +1 -1
- basic_memory/mcp/async_client.py +0 -2
- basic_memory/mcp/server.py +3 -27
- basic_memory/mcp/tools/__init__.py +5 -3
- basic_memory/mcp/tools/knowledge.py +2 -2
- basic_memory/mcp/tools/memory.py +8 -4
- basic_memory/mcp/tools/search.py +2 -1
- basic_memory/mcp/tools/utils.py +1 -1
- basic_memory/models/__init__.py +1 -2
- basic_memory/models/base.py +3 -3
- basic_memory/models/knowledge.py +23 -60
- basic_memory/models/search.py +1 -1
- basic_memory/repository/__init__.py +5 -3
- basic_memory/repository/entity_repository.py +34 -98
- basic_memory/repository/relation_repository.py +0 -7
- basic_memory/repository/repository.py +2 -39
- basic_memory/repository/search_repository.py +20 -25
- basic_memory/schemas/__init__.py +4 -4
- basic_memory/schemas/base.py +21 -62
- basic_memory/schemas/delete.py +2 -3
- basic_memory/schemas/discovery.py +4 -1
- basic_memory/schemas/memory.py +12 -13
- basic_memory/schemas/request.py +4 -23
- basic_memory/schemas/response.py +10 -9
- basic_memory/schemas/search.py +4 -7
- basic_memory/services/__init__.py +2 -7
- basic_memory/services/context_service.py +116 -110
- basic_memory/services/entity_service.py +25 -62
- basic_memory/services/exceptions.py +1 -0
- basic_memory/services/file_service.py +73 -109
- basic_memory/services/link_resolver.py +9 -9
- basic_memory/services/search_service.py +22 -15
- basic_memory/services/service.py +3 -24
- basic_memory/sync/__init__.py +2 -2
- basic_memory/sync/file_change_scanner.py +3 -7
- basic_memory/sync/sync_service.py +35 -40
- basic_memory/sync/utils.py +6 -38
- basic_memory/sync/watch_service.py +26 -5
- basic_memory/utils.py +42 -33
- {basic_memory-0.1.1.dist-info → basic_memory-0.1.2.dist-info}/METADATA +2 -7
- basic_memory-0.1.2.dist-info/RECORD +78 -0
- basic_memory/mcp/main.py +0 -21
- basic_memory/mcp/tools/ai_edit.py +0 -84
- basic_memory/services/database_service.py +0 -159
- basic_memory-0.1.1.dist-info/RECORD +0 -74
- {basic_memory-0.1.1.dist-info → basic_memory-0.1.2.dist-info}/WHEEL +0 -0
- {basic_memory-0.1.1.dist-info → basic_memory-0.1.2.dist-info}/entry_points.txt +0 -0
- {basic_memory-0.1.1.dist-info → basic_memory-0.1.2.dist-info}/licenses/LICENSE +0 -0
basic_memory/markdown/schemas.py
CHANGED
|
@@ -13,7 +13,7 @@ class Observation(BaseModel):
|
|
|
13
13
|
content: str
|
|
14
14
|
tags: Optional[List[str]] = None
|
|
15
15
|
context: Optional[str] = None
|
|
16
|
-
|
|
16
|
+
|
|
17
17
|
def __str__(self) -> str:
|
|
18
18
|
obs_string = f"- [{self.category}] {self.content}"
|
|
19
19
|
if self.context:
|
|
@@ -27,7 +27,7 @@ class Relation(BaseModel):
|
|
|
27
27
|
type: str
|
|
28
28
|
target: str
|
|
29
29
|
context: Optional[str] = None
|
|
30
|
-
|
|
30
|
+
|
|
31
31
|
def __str__(self) -> str:
|
|
32
32
|
rel_string = f"- {self.type} [[{self.target}]]"
|
|
33
33
|
if self.context:
|
|
@@ -38,24 +38,23 @@ class Relation(BaseModel):
|
|
|
38
38
|
class EntityFrontmatter(BaseModel):
|
|
39
39
|
"""Required frontmatter fields for an entity."""
|
|
40
40
|
|
|
41
|
-
metadata:
|
|
41
|
+
metadata: dict = {}
|
|
42
42
|
|
|
43
43
|
@property
|
|
44
44
|
def tags(self) -> List[str]:
|
|
45
|
-
return self.metadata.get("tags") if self.metadata else []
|
|
45
|
+
return self.metadata.get("tags") if self.metadata else [] # pyright: ignore
|
|
46
46
|
|
|
47
47
|
@property
|
|
48
48
|
def title(self) -> str:
|
|
49
|
-
return self.metadata.get("title") if self.metadata else None
|
|
49
|
+
return self.metadata.get("title") if self.metadata else None # pyright: ignore
|
|
50
50
|
|
|
51
51
|
@property
|
|
52
52
|
def type(self) -> str:
|
|
53
|
-
return self.metadata.get("type", "note") if self.metadata else "note"
|
|
53
|
+
return self.metadata.get("type", "note") if self.metadata else "note" # pyright: ignore
|
|
54
54
|
|
|
55
55
|
@property
|
|
56
56
|
def permalink(self) -> str:
|
|
57
|
-
return self.metadata.get("permalink") if self.metadata else None
|
|
58
|
-
|
|
57
|
+
return self.metadata.get("permalink") if self.metadata else None # pyright: ignore
|
|
59
58
|
|
|
60
59
|
|
|
61
60
|
class EntityMarkdown(BaseModel):
|
basic_memory/markdown/utils.py
CHANGED
|
@@ -1,144 +1,93 @@
|
|
|
1
|
+
"""Utilities for converting between markdown and entity models."""
|
|
2
|
+
|
|
1
3
|
from pathlib import Path
|
|
2
|
-
from typing import Optional
|
|
4
|
+
from typing import Optional, Any
|
|
3
5
|
|
|
4
6
|
from frontmatter import Post
|
|
5
7
|
|
|
6
|
-
from basic_memory.markdown import EntityMarkdown
|
|
7
|
-
from basic_memory.
|
|
8
|
-
from basic_memory.models import Entity, ObservationCategory, Observation as ObservationModel
|
|
8
|
+
from basic_memory.markdown import EntityMarkdown
|
|
9
|
+
from basic_memory.models import Entity, Observation as ObservationModel
|
|
9
10
|
from basic_memory.utils import generate_permalink
|
|
10
11
|
|
|
11
12
|
|
|
12
|
-
def
|
|
13
|
+
def entity_model_from_markdown(
|
|
14
|
+
file_path: Path, markdown: EntityMarkdown, entity: Optional[Entity] = None
|
|
15
|
+
) -> Entity:
|
|
13
16
|
"""
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
:return: An instance of the EntityMarkdown class containing the entity's
|
|
27
|
-
frontmatter, observations, relations, and sanitized content formatted
|
|
28
|
-
in Markdown.
|
|
29
|
-
:rtype: EntityMarkdown
|
|
17
|
+
Convert markdown entity to model. Does not include relations.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
file_path: Path to the markdown file
|
|
21
|
+
markdown: Parsed markdown entity
|
|
22
|
+
entity: Optional existing entity to update
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Entity model populated from markdown
|
|
26
|
+
|
|
27
|
+
Raises:
|
|
28
|
+
ValueError: If required datetime fields are missing from markdown
|
|
30
29
|
"""
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
30
|
+
|
|
31
|
+
if not markdown.created or not markdown.modified: # pragma: no cover
|
|
32
|
+
raise ValueError("Both created and modified dates are required in markdown")
|
|
33
|
+
|
|
34
|
+
# Generate permalink if not provided
|
|
35
|
+
permalink = markdown.frontmatter.permalink or generate_permalink(file_path)
|
|
36
|
+
|
|
37
|
+
# Create or update entity
|
|
38
|
+
model = entity or Entity()
|
|
39
|
+
|
|
40
|
+
# Update basic fields
|
|
41
|
+
model.title = markdown.frontmatter.title
|
|
42
|
+
model.entity_type = markdown.frontmatter.type
|
|
43
|
+
model.permalink = permalink
|
|
44
|
+
model.file_path = str(file_path)
|
|
45
|
+
model.content_type = "text/markdown"
|
|
46
|
+
model.created_at = markdown.created
|
|
47
|
+
model.updated_at = markdown.modified
|
|
48
|
+
|
|
49
|
+
# Handle metadata - ensure all values are strings and filter None
|
|
50
|
+
metadata = markdown.frontmatter.metadata or {}
|
|
51
|
+
model.entity_metadata = {k: str(v) for k, v in metadata.items() if v is not None}
|
|
52
|
+
|
|
53
|
+
# Convert observations
|
|
54
|
+
model.observations = [
|
|
55
|
+
ObservationModel(
|
|
40
56
|
content=obs.content,
|
|
41
|
-
|
|
57
|
+
category=obs.category,
|
|
42
58
|
context=obs.context,
|
|
59
|
+
tags=obs.tags,
|
|
43
60
|
)
|
|
44
|
-
for obs in
|
|
61
|
+
for obs in markdown.observations
|
|
45
62
|
]
|
|
46
63
|
|
|
47
|
-
|
|
48
|
-
Relation(
|
|
49
|
-
type=r.relation_type,
|
|
50
|
-
target=r.to_entity.title if r.to_entity else r.to_name,
|
|
51
|
-
context=r.context,
|
|
52
|
-
)
|
|
53
|
-
for r in entity.outgoing_relations
|
|
54
|
-
]
|
|
55
|
-
|
|
56
|
-
observations = entity_observations
|
|
57
|
-
relations = entity_relations
|
|
58
|
-
|
|
59
|
-
# parse the content to see if it has semantic info (observations/relations)
|
|
60
|
-
entity_content = parse(content) if content else None
|
|
61
|
-
|
|
62
|
-
if entity_content:
|
|
63
|
-
# remove if they are already in the content
|
|
64
|
-
observations = [o for o in entity_observations if o not in entity_content.observations]
|
|
65
|
-
relations = [r for r in entity_relations if r not in entity_content.relations]
|
|
66
|
-
|
|
67
|
-
# remove from the content if not present in the db entity
|
|
68
|
-
for o in entity_content.observations:
|
|
69
|
-
if o not in entity_observations:
|
|
70
|
-
content = content.replace(str(o), "")
|
|
71
|
-
|
|
72
|
-
for r in entity_content.relations:
|
|
73
|
-
if r not in entity_relations:
|
|
74
|
-
content = content.replace(str(r), "")
|
|
75
|
-
|
|
76
|
-
return EntityMarkdown(
|
|
77
|
-
frontmatter=EntityFrontmatter(metadata=metadata),
|
|
78
|
-
content=content,
|
|
79
|
-
observations=observations,
|
|
80
|
-
relations=relations,
|
|
81
|
-
created = entity.created_at,
|
|
82
|
-
modified = entity.updated_at,
|
|
83
|
-
)
|
|
64
|
+
return model
|
|
84
65
|
|
|
85
66
|
|
|
86
|
-
def
|
|
67
|
+
async def schema_to_markdown(schema: Any) -> Post:
|
|
87
68
|
"""
|
|
88
|
-
Convert markdown
|
|
89
|
-
Does not include relations.
|
|
69
|
+
Convert schema to markdown Post object.
|
|
90
70
|
|
|
91
71
|
Args:
|
|
92
|
-
|
|
93
|
-
include_relations: Whether to include relations. Set False for first sync pass.
|
|
94
|
-
"""
|
|
72
|
+
schema: Schema to convert (must have title, entity_type, and permalink attributes)
|
|
95
73
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if not obs.category or obs.category not in [c.value for c in ObservationCategory]:
|
|
99
|
-
return ObservationCategory.NOTE.value
|
|
100
|
-
return obs.category
|
|
101
|
-
|
|
102
|
-
permalink = markdown.frontmatter.permalink or generate_permalink(file_path)
|
|
103
|
-
model = entity or Entity()
|
|
104
|
-
|
|
105
|
-
model.title=markdown.frontmatter.title
|
|
106
|
-
model.entity_type=markdown.frontmatter.type
|
|
107
|
-
model.permalink=permalink
|
|
108
|
-
model.file_path=str(file_path)
|
|
109
|
-
model.content_type="text/markdown"
|
|
110
|
-
model.created_at=markdown.created
|
|
111
|
-
model.updated_at=markdown.modified
|
|
112
|
-
model.entity_metadata={k:str(v) for k,v in markdown.frontmatter.metadata.items()}
|
|
113
|
-
model.observations=[
|
|
114
|
-
ObservationModel(
|
|
115
|
-
content=obs.content,
|
|
116
|
-
category=get_valid_category(obs),
|
|
117
|
-
context=obs.context,
|
|
118
|
-
tags=obs.tags,
|
|
119
|
-
)
|
|
120
|
-
for obs in markdown.observations
|
|
121
|
-
]
|
|
122
|
-
|
|
123
|
-
return model
|
|
124
|
-
|
|
125
|
-
async def schema_to_markdown(schema):
|
|
126
|
-
"""
|
|
127
|
-
Convert schema to markdown.
|
|
128
|
-
:param schema: the schema to convert
|
|
129
|
-
:return: Post
|
|
74
|
+
Returns:
|
|
75
|
+
Post object with frontmatter metadata
|
|
130
76
|
"""
|
|
131
|
-
#
|
|
77
|
+
# Extract content and metadata
|
|
132
78
|
content = schema.content or ""
|
|
133
|
-
frontmatter_metadata = schema.entity_metadata or {}
|
|
134
|
-
|
|
135
|
-
#
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
79
|
+
frontmatter_metadata = dict(schema.entity_metadata or {})
|
|
80
|
+
|
|
81
|
+
# Remove special fields for ordered frontmatter
|
|
82
|
+
for field in ["type", "title", "permalink"]:
|
|
83
|
+
frontmatter_metadata.pop(field, None)
|
|
84
|
+
|
|
85
|
+
# Create Post with ordered fields
|
|
86
|
+
post = Post(
|
|
87
|
+
content,
|
|
88
|
+
title=schema.title,
|
|
89
|
+
type=schema.entity_type,
|
|
90
|
+
permalink=schema.permalink,
|
|
91
|
+
**frontmatter_metadata,
|
|
92
|
+
)
|
|
144
93
|
return post
|
basic_memory/mcp/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"""MCP server for basic-memory."""
|
|
1
|
+
"""MCP server for basic-memory."""
|
basic_memory/mcp/async_client.py
CHANGED
basic_memory/mcp/server.py
CHANGED
|
@@ -1,39 +1,15 @@
|
|
|
1
1
|
"""Enhanced FastMCP server instance for Basic Memory."""
|
|
2
|
-
import sys
|
|
3
2
|
|
|
4
|
-
from loguru import logger
|
|
5
3
|
from mcp.server.fastmcp import FastMCP
|
|
6
|
-
from mcp.server.fastmcp.utilities.logging import configure_logging
|
|
7
4
|
|
|
8
|
-
from basic_memory.
|
|
5
|
+
from basic_memory.utils import setup_logging
|
|
9
6
|
|
|
10
7
|
# mcp console logging
|
|
11
|
-
configure_logging(level=
|
|
8
|
+
# configure_logging(level='INFO')
|
|
12
9
|
|
|
13
10
|
|
|
14
|
-
def setup_logging(home_dir: str = config.home, log_file: str = ".basic-memory/basic-memory.log"):
|
|
15
|
-
"""Configure file logging to the basic-memory home directory."""
|
|
16
|
-
log = f"{home_dir}/{log_file}"
|
|
17
|
-
|
|
18
|
-
# Add file handler with rotation
|
|
19
|
-
logger.add(
|
|
20
|
-
log,
|
|
21
|
-
rotation="100 MB",
|
|
22
|
-
retention="10 days",
|
|
23
|
-
backtrace=True,
|
|
24
|
-
diagnose=True,
|
|
25
|
-
enqueue=True,
|
|
26
|
-
colorize=False,
|
|
27
|
-
)
|
|
28
|
-
|
|
29
|
-
# Add stderr handler
|
|
30
|
-
logger.add(
|
|
31
|
-
sys.stderr,
|
|
32
|
-
colorize=True,
|
|
33
|
-
)
|
|
34
|
-
|
|
35
11
|
# start our out file logging
|
|
36
|
-
setup_logging()
|
|
12
|
+
setup_logging(log_file=".basic-memory/basic-memory.log")
|
|
37
13
|
|
|
38
14
|
# Create the shared server instance
|
|
39
15
|
mcp = FastMCP("Basic Memory")
|
|
@@ -7,8 +7,10 @@ all tools with the MCP server.
|
|
|
7
7
|
|
|
8
8
|
# Import tools to register them with MCP
|
|
9
9
|
from basic_memory.mcp.tools.memory import build_context, recent_activity
|
|
10
|
-
|
|
10
|
+
|
|
11
|
+
# from basic_memory.mcp.tools.ai_edit import ai_edit
|
|
11
12
|
from basic_memory.mcp.tools.notes import read_note, write_note
|
|
13
|
+
from basic_memory.mcp.tools.search import search
|
|
12
14
|
|
|
13
15
|
from basic_memory.mcp.tools.knowledge import (
|
|
14
16
|
delete_entities,
|
|
@@ -26,9 +28,9 @@ __all__ = [
|
|
|
26
28
|
# memory tools
|
|
27
29
|
"build_context",
|
|
28
30
|
"recent_activity",
|
|
29
|
-
#notes
|
|
31
|
+
# notes
|
|
30
32
|
"read_note",
|
|
31
33
|
"write_note",
|
|
32
34
|
# file edit
|
|
33
|
-
#"ai_edit",
|
|
35
|
+
# "ai_edit",
|
|
34
36
|
]
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from basic_memory.mcp.server import mcp
|
|
4
4
|
from basic_memory.mcp.tools.utils import call_get, call_post
|
|
5
|
-
from basic_memory.schemas.base import
|
|
5
|
+
from basic_memory.schemas.base import Permalink
|
|
6
6
|
from basic_memory.schemas.request import (
|
|
7
7
|
GetEntitiesRequest,
|
|
8
8
|
)
|
|
@@ -16,7 +16,7 @@ from basic_memory.mcp.async_client import client
|
|
|
16
16
|
@mcp.tool(
|
|
17
17
|
description="Get complete information about a specific entity including observations and relations",
|
|
18
18
|
)
|
|
19
|
-
async def get_entity(permalink:
|
|
19
|
+
async def get_entity(permalink: Permalink) -> EntityResponse:
|
|
20
20
|
"""Get a specific entity info by its permalink.
|
|
21
21
|
|
|
22
22
|
Args:
|
basic_memory/mcp/tools/memory.py
CHANGED
|
@@ -7,9 +7,13 @@ from loguru import logger
|
|
|
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_get
|
|
10
|
-
from basic_memory.schemas.memory import
|
|
10
|
+
from basic_memory.schemas.memory import (
|
|
11
|
+
GraphContext,
|
|
12
|
+
MemoryUrl,
|
|
13
|
+
memory_url_path,
|
|
14
|
+
normalize_memory_url,
|
|
15
|
+
)
|
|
11
16
|
from basic_memory.schemas.base import TimeFrame
|
|
12
|
-
from basic_memory.schemas.search import SearchItemType
|
|
13
17
|
|
|
14
18
|
|
|
15
19
|
@mcp.tool(
|
|
@@ -84,7 +88,7 @@ async def build_context(
|
|
|
84
88
|
""",
|
|
85
89
|
)
|
|
86
90
|
async def recent_activity(
|
|
87
|
-
type: List[Literal["entity", "observation", "relation"]] =
|
|
91
|
+
type: List[Literal["entity", "observation", "relation"]] = [],
|
|
88
92
|
depth: Optional[int] = 1,
|
|
89
93
|
timeframe: Optional[TimeFrame] = "7d",
|
|
90
94
|
max_results: int = 10,
|
|
@@ -136,7 +140,7 @@ async def recent_activity(
|
|
|
136
140
|
"timeframe": timeframe,
|
|
137
141
|
"max_results": max_results,
|
|
138
142
|
}
|
|
139
|
-
if type:
|
|
143
|
+
if type:
|
|
140
144
|
params["type"] = type
|
|
141
145
|
|
|
142
146
|
response = await call_get(
|
basic_memory/mcp/tools/search.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""Search tools for Basic Memory MCP server."""
|
|
2
|
+
|
|
2
3
|
from loguru import logger
|
|
3
4
|
|
|
4
5
|
from basic_memory.mcp.server import mcp
|
|
@@ -24,5 +25,5 @@ async def search(query: SearchQuery) -> SearchResponse:
|
|
|
24
25
|
SearchResponse with search results and metadata
|
|
25
26
|
"""
|
|
26
27
|
logger.info(f"Searching for {query.text}")
|
|
27
|
-
response = await call_post(client,"/search/", json=query.model_dump())
|
|
28
|
+
response = await call_post(client, "/search/", json=query.model_dump())
|
|
28
29
|
return SearchResponse.model_validate(response.json())
|
basic_memory/mcp/tools/utils.py
CHANGED
|
@@ -45,7 +45,7 @@ async def call_get(
|
|
|
45
45
|
return response
|
|
46
46
|
except HTTPStatusError as e:
|
|
47
47
|
logger.error(f"Error calling GET {url}: {e}")
|
|
48
|
-
raise ToolError(f"Error calling tool: {e}.
|
|
48
|
+
raise ToolError(f"Error calling tool: {e}.") from e
|
|
49
49
|
|
|
50
50
|
|
|
51
51
|
async def call_put(
|
basic_memory/models/__init__.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import basic_memory
|
|
4
4
|
from basic_memory.models.base import Base
|
|
5
|
-
from basic_memory.models.knowledge import Entity, Observation, Relation
|
|
5
|
+
from basic_memory.models.knowledge import Entity, Observation, Relation
|
|
6
6
|
|
|
7
7
|
SCHEMA_VERSION = basic_memory.__version__ + "-" + "003"
|
|
8
8
|
|
|
@@ -10,6 +10,5 @@ __all__ = [
|
|
|
10
10
|
"Base",
|
|
11
11
|
"Entity",
|
|
12
12
|
"Observation",
|
|
13
|
-
"ObservationCategory",
|
|
14
13
|
"Relation",
|
|
15
14
|
]
|
basic_memory/models/base.py
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
"""Base model class for SQLAlchemy models."""
|
|
2
|
-
|
|
2
|
+
|
|
3
3
|
from sqlalchemy.ext.asyncio import AsyncAttrs
|
|
4
|
-
from sqlalchemy.orm import DeclarativeBase
|
|
4
|
+
from sqlalchemy.orm import DeclarativeBase
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
class Base(AsyncAttrs, DeclarativeBase):
|
|
8
8
|
"""Base class for all models"""
|
|
9
|
-
pass
|
|
10
9
|
|
|
10
|
+
pass
|
basic_memory/models/knowledge.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
"""Knowledge graph models."""
|
|
2
2
|
|
|
3
|
-
import re
|
|
4
3
|
from datetime import datetime
|
|
5
4
|
from typing import Optional
|
|
6
5
|
|
|
@@ -14,17 +13,15 @@ from sqlalchemy import (
|
|
|
14
13
|
Index,
|
|
15
14
|
JSON,
|
|
16
15
|
)
|
|
17
|
-
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
16
|
+
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
18
17
|
|
|
19
18
|
from basic_memory.models.base import Base
|
|
20
|
-
from enum import Enum
|
|
21
19
|
|
|
22
20
|
from basic_memory.utils import generate_permalink
|
|
23
21
|
|
|
24
22
|
|
|
25
23
|
class Entity(Base):
|
|
26
|
-
"""
|
|
27
|
-
Core entity in the knowledge graph.
|
|
24
|
+
"""Core entity in the knowledge graph.
|
|
28
25
|
|
|
29
26
|
Entities represent semantic nodes maintained by the AI layer. Each entity:
|
|
30
27
|
- Has a unique numeric ID (database-generated)
|
|
@@ -79,44 +76,15 @@ class Entity(Base):
|
|
|
79
76
|
|
|
80
77
|
@property
|
|
81
78
|
def relations(self):
|
|
79
|
+
"""Get all relations (incoming and outgoing) for this entity."""
|
|
82
80
|
return self.incoming_relations + self.outgoing_relations
|
|
83
81
|
|
|
84
|
-
@validates("permalink")
|
|
85
|
-
def validate_permalink(self, key, value):
|
|
86
|
-
"""Validate permalink format.
|
|
87
|
-
|
|
88
|
-
Requirements:
|
|
89
|
-
1. Must be valid URI path component
|
|
90
|
-
2. Only lowercase letters, numbers, and hyphens (no underscores)
|
|
91
|
-
3. Path segments separated by forward slashes
|
|
92
|
-
4. No leading/trailing hyphens in segments
|
|
93
|
-
"""
|
|
94
|
-
if not value:
|
|
95
|
-
raise ValueError("Permalink must not be None")
|
|
96
|
-
|
|
97
|
-
if not re.match(r"^[a-z0-9][a-z0-9\-/]*[a-z0-9]$", value):
|
|
98
|
-
raise ValueError(
|
|
99
|
-
f"Invalid permalink format: {value}. "
|
|
100
|
-
"Use only lowercase letters, numbers, and hyphens."
|
|
101
|
-
)
|
|
102
|
-
return value
|
|
103
|
-
|
|
104
82
|
def __repr__(self) -> str:
|
|
105
83
|
return f"Entity(id={self.id}, name='{self.title}', type='{self.entity_type}'"
|
|
106
84
|
|
|
107
85
|
|
|
108
|
-
class ObservationCategory(str, Enum):
|
|
109
|
-
TECH = "tech"
|
|
110
|
-
DESIGN = "design"
|
|
111
|
-
FEATURE = "feature"
|
|
112
|
-
NOTE = "note"
|
|
113
|
-
ISSUE = "issue"
|
|
114
|
-
TODO = "todo"
|
|
115
|
-
|
|
116
|
-
|
|
117
86
|
class Observation(Base):
|
|
118
|
-
"""
|
|
119
|
-
An observation about an entity.
|
|
87
|
+
"""An observation about an entity.
|
|
120
88
|
|
|
121
89
|
Observations are atomic facts or notes about an entity.
|
|
122
90
|
"""
|
|
@@ -130,13 +98,8 @@ class Observation(Base):
|
|
|
130
98
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
131
99
|
entity_id: Mapped[int] = mapped_column(Integer, ForeignKey("entity.id", ondelete="CASCADE"))
|
|
132
100
|
content: Mapped[str] = mapped_column(Text)
|
|
133
|
-
category: Mapped[str] = mapped_column(
|
|
134
|
-
|
|
135
|
-
nullable=False,
|
|
136
|
-
default=ObservationCategory.NOTE.value,
|
|
137
|
-
server_default=ObservationCategory.NOTE.value,
|
|
138
|
-
)
|
|
139
|
-
context: Mapped[str] = mapped_column(Text, nullable=True)
|
|
101
|
+
category: Mapped[str] = mapped_column(String, nullable=False, default="note")
|
|
102
|
+
context: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
|
140
103
|
tags: Mapped[Optional[list[str]]] = mapped_column(
|
|
141
104
|
JSON, nullable=True, default=list, server_default="[]"
|
|
142
105
|
)
|
|
@@ -146,23 +109,21 @@ class Observation(Base):
|
|
|
146
109
|
|
|
147
110
|
@property
|
|
148
111
|
def permalink(self) -> str:
|
|
149
|
-
"""
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
112
|
+
"""Create synthetic permalink for the observation.
|
|
113
|
+
|
|
114
|
+
We can construct these because observations are always defined in
|
|
115
|
+
and owned by a single entity.
|
|
153
116
|
"""
|
|
154
117
|
return generate_permalink(
|
|
155
118
|
f"{self.entity.permalink}/observations/{self.category}/{self.content}"
|
|
156
119
|
)
|
|
157
120
|
|
|
158
|
-
def __repr__(self) -> str:
|
|
121
|
+
def __repr__(self) -> str: # pragma: no cover
|
|
159
122
|
return f"Observation(id={self.id}, entity_id={self.entity_id}, content='{self.content}')"
|
|
160
123
|
|
|
161
124
|
|
|
162
125
|
class Relation(Base):
|
|
163
|
-
"""
|
|
164
|
-
A directed relation between two entities.
|
|
165
|
-
"""
|
|
126
|
+
"""A directed relation between two entities."""
|
|
166
127
|
|
|
167
128
|
__tablename__ = "relation"
|
|
168
129
|
__table_args__ = (
|
|
@@ -174,12 +135,12 @@ class Relation(Base):
|
|
|
174
135
|
|
|
175
136
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
176
137
|
from_id: Mapped[int] = mapped_column(Integer, ForeignKey("entity.id", ondelete="CASCADE"))
|
|
177
|
-
to_id: Mapped[int] = mapped_column(
|
|
138
|
+
to_id: Mapped[Optional[int]] = mapped_column(
|
|
178
139
|
Integer, ForeignKey("entity.id", ondelete="CASCADE"), nullable=True
|
|
179
140
|
)
|
|
180
141
|
to_name: Mapped[str] = mapped_column(String)
|
|
181
142
|
relation_type: Mapped[str] = mapped_column(String)
|
|
182
|
-
context: Mapped[str] = mapped_column(Text, nullable=True)
|
|
143
|
+
context: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
|
183
144
|
|
|
184
145
|
# Relationships
|
|
185
146
|
from_entity = relationship(
|
|
@@ -189,15 +150,17 @@ class Relation(Base):
|
|
|
189
150
|
|
|
190
151
|
@property
|
|
191
152
|
def permalink(self) -> str:
|
|
192
|
-
"""Create relation permalink showing the semantic connection
|
|
193
|
-
source/relation_type/target
|
|
194
|
-
e.g., "specs/search/implements/features/search-ui"
|
|
195
|
-
"""
|
|
153
|
+
"""Create relation permalink showing the semantic connection.
|
|
196
154
|
|
|
155
|
+
Format: source/relation_type/target
|
|
156
|
+
Example: "specs/search/implements/features/search-ui"
|
|
157
|
+
"""
|
|
158
|
+
if self.to_entity:
|
|
159
|
+
return generate_permalink(
|
|
160
|
+
f"{self.from_entity.permalink}/{self.relation_type}/{self.to_entity.permalink}"
|
|
161
|
+
)
|
|
197
162
|
return generate_permalink(
|
|
198
|
-
f"{self.from_entity.permalink}/{self.relation_type}/{self.
|
|
199
|
-
if self.to_entity
|
|
200
|
-
else f"{self.from_entity.permalink}/{self.relation_type}/{self.to_name}"
|
|
163
|
+
f"{self.from_entity.permalink}/{self.relation_type}/{self.to_name}"
|
|
201
164
|
)
|
|
202
165
|
|
|
203
166
|
def __repr__(self) -> str:
|
basic_memory/models/search.py
CHANGED
|
@@ -2,6 +2,8 @@ from .entity_repository import EntityRepository
|
|
|
2
2
|
from .observation_repository import ObservationRepository
|
|
3
3
|
from .relation_repository import RelationRepository
|
|
4
4
|
|
|
5
|
-
__all__ = [
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
__all__ = [
|
|
6
|
+
"EntityRepository",
|
|
7
|
+
"ObservationRepository",
|
|
8
|
+
"RelationRepository",
|
|
9
|
+
]
|