remdb 0.3.7__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 (187) hide show
  1. rem/__init__.py +2 -0
  2. rem/agentic/README.md +650 -0
  3. rem/agentic/__init__.py +39 -0
  4. rem/agentic/agents/README.md +155 -0
  5. rem/agentic/agents/__init__.py +8 -0
  6. rem/agentic/context.py +148 -0
  7. rem/agentic/context_builder.py +329 -0
  8. rem/agentic/mcp/__init__.py +0 -0
  9. rem/agentic/mcp/tool_wrapper.py +107 -0
  10. rem/agentic/otel/__init__.py +5 -0
  11. rem/agentic/otel/setup.py +151 -0
  12. rem/agentic/providers/phoenix.py +674 -0
  13. rem/agentic/providers/pydantic_ai.py +572 -0
  14. rem/agentic/query.py +117 -0
  15. rem/agentic/query_helper.py +89 -0
  16. rem/agentic/schema.py +396 -0
  17. rem/agentic/serialization.py +245 -0
  18. rem/agentic/tools/__init__.py +5 -0
  19. rem/agentic/tools/rem_tools.py +231 -0
  20. rem/api/README.md +420 -0
  21. rem/api/main.py +324 -0
  22. rem/api/mcp_router/prompts.py +182 -0
  23. rem/api/mcp_router/resources.py +536 -0
  24. rem/api/mcp_router/server.py +213 -0
  25. rem/api/mcp_router/tools.py +584 -0
  26. rem/api/routers/auth.py +229 -0
  27. rem/api/routers/chat/__init__.py +5 -0
  28. rem/api/routers/chat/completions.py +281 -0
  29. rem/api/routers/chat/json_utils.py +76 -0
  30. rem/api/routers/chat/models.py +124 -0
  31. rem/api/routers/chat/streaming.py +185 -0
  32. rem/auth/README.md +258 -0
  33. rem/auth/__init__.py +26 -0
  34. rem/auth/middleware.py +100 -0
  35. rem/auth/providers/__init__.py +13 -0
  36. rem/auth/providers/base.py +376 -0
  37. rem/auth/providers/google.py +163 -0
  38. rem/auth/providers/microsoft.py +237 -0
  39. rem/cli/README.md +455 -0
  40. rem/cli/__init__.py +8 -0
  41. rem/cli/commands/README.md +126 -0
  42. rem/cli/commands/__init__.py +3 -0
  43. rem/cli/commands/ask.py +566 -0
  44. rem/cli/commands/configure.py +497 -0
  45. rem/cli/commands/db.py +493 -0
  46. rem/cli/commands/dreaming.py +324 -0
  47. rem/cli/commands/experiments.py +1302 -0
  48. rem/cli/commands/mcp.py +66 -0
  49. rem/cli/commands/process.py +245 -0
  50. rem/cli/commands/schema.py +183 -0
  51. rem/cli/commands/serve.py +106 -0
  52. rem/cli/dreaming.py +363 -0
  53. rem/cli/main.py +96 -0
  54. rem/config.py +237 -0
  55. rem/mcp_server.py +41 -0
  56. rem/models/core/__init__.py +49 -0
  57. rem/models/core/core_model.py +64 -0
  58. rem/models/core/engram.py +333 -0
  59. rem/models/core/experiment.py +628 -0
  60. rem/models/core/inline_edge.py +132 -0
  61. rem/models/core/rem_query.py +243 -0
  62. rem/models/entities/__init__.py +43 -0
  63. rem/models/entities/file.py +57 -0
  64. rem/models/entities/image_resource.py +88 -0
  65. rem/models/entities/message.py +35 -0
  66. rem/models/entities/moment.py +123 -0
  67. rem/models/entities/ontology.py +191 -0
  68. rem/models/entities/ontology_config.py +131 -0
  69. rem/models/entities/resource.py +95 -0
  70. rem/models/entities/schema.py +87 -0
  71. rem/models/entities/user.py +85 -0
  72. rem/py.typed +0 -0
  73. rem/schemas/README.md +507 -0
  74. rem/schemas/__init__.py +6 -0
  75. rem/schemas/agents/README.md +92 -0
  76. rem/schemas/agents/core/moment-builder.yaml +178 -0
  77. rem/schemas/agents/core/rem-query-agent.yaml +226 -0
  78. rem/schemas/agents/core/resource-affinity-assessor.yaml +99 -0
  79. rem/schemas/agents/core/simple-assistant.yaml +19 -0
  80. rem/schemas/agents/core/user-profile-builder.yaml +163 -0
  81. rem/schemas/agents/examples/contract-analyzer.yaml +317 -0
  82. rem/schemas/agents/examples/contract-extractor.yaml +134 -0
  83. rem/schemas/agents/examples/cv-parser.yaml +263 -0
  84. rem/schemas/agents/examples/hello-world.yaml +37 -0
  85. rem/schemas/agents/examples/query.yaml +54 -0
  86. rem/schemas/agents/examples/simple.yaml +21 -0
  87. rem/schemas/agents/examples/test.yaml +29 -0
  88. rem/schemas/agents/rem.yaml +128 -0
  89. rem/schemas/evaluators/hello-world/default.yaml +77 -0
  90. rem/schemas/evaluators/rem/faithfulness.yaml +219 -0
  91. rem/schemas/evaluators/rem/lookup-correctness.yaml +182 -0
  92. rem/schemas/evaluators/rem/retrieval-precision.yaml +199 -0
  93. rem/schemas/evaluators/rem/retrieval-recall.yaml +211 -0
  94. rem/schemas/evaluators/rem/search-correctness.yaml +192 -0
  95. rem/services/__init__.py +16 -0
  96. rem/services/audio/INTEGRATION.md +308 -0
  97. rem/services/audio/README.md +376 -0
  98. rem/services/audio/__init__.py +15 -0
  99. rem/services/audio/chunker.py +354 -0
  100. rem/services/audio/transcriber.py +259 -0
  101. rem/services/content/README.md +1269 -0
  102. rem/services/content/__init__.py +5 -0
  103. rem/services/content/providers.py +801 -0
  104. rem/services/content/service.py +676 -0
  105. rem/services/dreaming/README.md +230 -0
  106. rem/services/dreaming/__init__.py +53 -0
  107. rem/services/dreaming/affinity_service.py +336 -0
  108. rem/services/dreaming/moment_service.py +264 -0
  109. rem/services/dreaming/ontology_service.py +54 -0
  110. rem/services/dreaming/user_model_service.py +297 -0
  111. rem/services/dreaming/utils.py +39 -0
  112. rem/services/embeddings/__init__.py +11 -0
  113. rem/services/embeddings/api.py +120 -0
  114. rem/services/embeddings/worker.py +421 -0
  115. rem/services/fs/README.md +662 -0
  116. rem/services/fs/__init__.py +62 -0
  117. rem/services/fs/examples.py +206 -0
  118. rem/services/fs/examples_paths.py +204 -0
  119. rem/services/fs/git_provider.py +935 -0
  120. rem/services/fs/local_provider.py +760 -0
  121. rem/services/fs/parsing-hooks-examples.md +172 -0
  122. rem/services/fs/paths.py +276 -0
  123. rem/services/fs/provider.py +460 -0
  124. rem/services/fs/s3_provider.py +1042 -0
  125. rem/services/fs/service.py +186 -0
  126. rem/services/git/README.md +1075 -0
  127. rem/services/git/__init__.py +17 -0
  128. rem/services/git/service.py +469 -0
  129. rem/services/phoenix/EXPERIMENT_DESIGN.md +1146 -0
  130. rem/services/phoenix/README.md +453 -0
  131. rem/services/phoenix/__init__.py +46 -0
  132. rem/services/phoenix/client.py +686 -0
  133. rem/services/phoenix/config.py +88 -0
  134. rem/services/phoenix/prompt_labels.py +477 -0
  135. rem/services/postgres/README.md +575 -0
  136. rem/services/postgres/__init__.py +23 -0
  137. rem/services/postgres/migration_service.py +427 -0
  138. rem/services/postgres/pydantic_to_sqlalchemy.py +232 -0
  139. rem/services/postgres/register_type.py +352 -0
  140. rem/services/postgres/repository.py +337 -0
  141. rem/services/postgres/schema_generator.py +379 -0
  142. rem/services/postgres/service.py +802 -0
  143. rem/services/postgres/sql_builder.py +354 -0
  144. rem/services/rem/README.md +304 -0
  145. rem/services/rem/__init__.py +23 -0
  146. rem/services/rem/exceptions.py +71 -0
  147. rem/services/rem/executor.py +293 -0
  148. rem/services/rem/parser.py +145 -0
  149. rem/services/rem/queries.py +196 -0
  150. rem/services/rem/query.py +371 -0
  151. rem/services/rem/service.py +527 -0
  152. rem/services/session/README.md +374 -0
  153. rem/services/session/__init__.py +6 -0
  154. rem/services/session/compression.py +360 -0
  155. rem/services/session/reload.py +77 -0
  156. rem/settings.py +1235 -0
  157. rem/sql/002_install_models.sql +1068 -0
  158. rem/sql/background_indexes.sql +42 -0
  159. rem/sql/install_models.sql +1038 -0
  160. rem/sql/migrations/001_install.sql +503 -0
  161. rem/sql/migrations/002_install_models.sql +1202 -0
  162. rem/utils/AGENTIC_CHUNKING.md +597 -0
  163. rem/utils/README.md +583 -0
  164. rem/utils/__init__.py +43 -0
  165. rem/utils/agentic_chunking.py +622 -0
  166. rem/utils/batch_ops.py +343 -0
  167. rem/utils/chunking.py +108 -0
  168. rem/utils/clip_embeddings.py +276 -0
  169. rem/utils/dict_utils.py +98 -0
  170. rem/utils/embeddings.py +423 -0
  171. rem/utils/examples/embeddings_example.py +305 -0
  172. rem/utils/examples/sql_types_example.py +202 -0
  173. rem/utils/markdown.py +16 -0
  174. rem/utils/model_helpers.py +236 -0
  175. rem/utils/schema_loader.py +336 -0
  176. rem/utils/sql_types.py +348 -0
  177. rem/utils/user_id.py +81 -0
  178. rem/utils/vision.py +330 -0
  179. rem/workers/README.md +506 -0
  180. rem/workers/__init__.py +5 -0
  181. rem/workers/dreaming.py +502 -0
  182. rem/workers/engram_processor.py +312 -0
  183. rem/workers/sqs_file_processor.py +193 -0
  184. remdb-0.3.7.dist-info/METADATA +1473 -0
  185. remdb-0.3.7.dist-info/RECORD +187 -0
  186. remdb-0.3.7.dist-info/WHEEL +4 -0
  187. remdb-0.3.7.dist-info/entry_points.txt +2 -0
rem/mcp_server.py ADDED
@@ -0,0 +1,41 @@
1
+ """
2
+ In-process MCP server for agent tool loading.
3
+
4
+ This module creates an MCP server instance that is imported directly by agents.
5
+ NO subprocess, NO stdio transport - just direct function calls in the same process.
6
+
7
+ The server exposes REM tools and resources that agents can call directly.
8
+ Services are initialized lazily on first tool call (see tools.py).
9
+ """
10
+
11
+ import asyncio
12
+
13
+ from rem.api.mcp_router.server import create_mcp_server
14
+
15
+ # Create MCP server instance (is_local=True for local/in-process usage)
16
+ # No initialization needed - tools handle lazy init of services
17
+ mcp = create_mcp_server(is_local=True)
18
+
19
+
20
+ if __name__ == "__main__":
21
+ # When run directly via CLI, start in stdio mode with service initialization
22
+ from rem.api.mcp_router.tools import init_services
23
+ from rem.services.postgres import get_postgres_service
24
+ from rem.services.rem import RemService
25
+
26
+ async def run_stdio():
27
+ """Run MCP server in stdio mode with services."""
28
+ db = get_postgres_service()
29
+ if not db:
30
+ raise RuntimeError("PostgreSQL service not available")
31
+ await db.connect()
32
+ rem_service = RemService(postgres_service=db)
33
+ init_services(postgres_service=db, rem_service=rem_service)
34
+
35
+ # Run server
36
+ await mcp.run_async(transport="stdio")
37
+
38
+ # Cleanup
39
+ await db.disconnect()
40
+
41
+ asyncio.run(run_stdio())
@@ -0,0 +1,49 @@
1
+ """
2
+ REM Core Models
3
+
4
+ Core types and base models for the REM (Resource-Entity-Moment) system.
5
+
6
+ REM is a unified memory infrastructure that enables LLM-augmented iterated retrieval
7
+ through natural language interfaces. Unlike traditional databases that assume single-shot
8
+ queries with known schemas, REM is architected for multi-turn conversations where:
9
+
10
+ 1. LLMs don't know internal IDs - they work with natural language labels
11
+ 2. Information needs emerge incrementally through exploration
12
+ 3. Multi-stage exploration is essential: find entity → explore neighborhood → traverse relationships
13
+
14
+ Key Design Principles:
15
+ - Graph edges reference entity LABELS (natural language), not UUIDs
16
+ - Natural language surface area for all queries
17
+ - Schema-agnostic operations (LOOKUP, FUZZY, TRAVERSE)
18
+ - O(1) performance guarantees for entity resolution
19
+ - Iterated retrieval with stage tracking and memos
20
+ """
21
+
22
+ from .core_model import CoreModel
23
+ from .inline_edge import InlineEdge, InlineEdges
24
+ from .rem_query import (
25
+ FuzzyParameters,
26
+ LookupParameters,
27
+ QueryType,
28
+ RemQuery,
29
+ SearchParameters,
30
+ SQLParameters,
31
+ TraverseParameters,
32
+ TraverseResponse,
33
+ TraverseStage,
34
+ )
35
+
36
+ __all__ = [
37
+ "CoreModel",
38
+ "InlineEdge",
39
+ "InlineEdges",
40
+ "QueryType",
41
+ "LookupParameters",
42
+ "FuzzyParameters",
43
+ "SearchParameters",
44
+ "SQLParameters",
45
+ "TraverseParameters",
46
+ "RemQuery",
47
+ "TraverseStage",
48
+ "TraverseResponse",
49
+ ]
@@ -0,0 +1,64 @@
1
+ """
2
+ CoreModel - Base model for all REM entities.
3
+
4
+ All REM entities (Resources, Messages, Users, Files, Moments) inherit from CoreModel,
5
+ which provides:
6
+ - Identity (id - UUID or string, generated per model type)
7
+ - Temporal tracking (created_at, updated_at, deleted_at)
8
+ - Multi-tenancy (tenant_id)
9
+ - Ownership (user_id)
10
+ - Graph connectivity (graph_edges)
11
+ - Flexible metadata (metadata dict)
12
+ - Tagging (tags list)
13
+ """
14
+
15
+ from datetime import datetime, timezone
16
+ from typing import Optional, Union
17
+ from uuid import UUID
18
+
19
+ from pydantic import BaseModel, ConfigDict, Field
20
+
21
+
22
+ class CoreModel(BaseModel):
23
+ """
24
+ Base model for all REM entities.
25
+
26
+ Provides system-level fields for:
27
+ - Identity management (id)
28
+ - Temporal tracking (created_at, updated_at, deleted_at)
29
+ - Multi-tenancy isolation (tenant_id)
30
+ - Ownership tracking (user_id)
31
+ - Graph connectivity (graph_edges)
32
+ - Flexible metadata storage (metadata, tags)
33
+
34
+ Note: ID generation is handled per model type, not by CoreModel.
35
+ Each entity model should generate IDs with appropriate prefixes or labels.
36
+ """
37
+
38
+ id: Union[UUID, str, None] = Field(
39
+ default=None,
40
+ description="Unique identifier (UUID or string, generated per model type). Generated automatically if not provided."
41
+ )
42
+ created_at: datetime = Field(
43
+ default_factory=lambda: datetime.now(timezone.utc).replace(tzinfo=None), description="Entity creation timestamp"
44
+ )
45
+ updated_at: datetime = Field(
46
+ default_factory=lambda: datetime.now(timezone.utc).replace(tzinfo=None), description="Last update timestamp"
47
+ )
48
+ deleted_at: Optional[datetime] = Field(
49
+ default=None, description="Soft deletion timestamp"
50
+ )
51
+ tenant_id: Optional[str] = Field(
52
+ default=None, description="Tenant identifier for multi-tenancy isolation"
53
+ )
54
+ user_id: Optional[str] = Field(
55
+ default=None, description="Owner user identifier (tenant-scoped)"
56
+ )
57
+ graph_edges: list[dict] = Field(
58
+ default_factory=list,
59
+ description="Knowledge graph edges stored as InlineEdge dicts",
60
+ )
61
+ metadata: dict = Field(
62
+ default_factory=dict, description="Flexible metadata storage"
63
+ )
64
+ tags: list[str] = Field(default_factory=list, description="Entity tags")
@@ -0,0 +1,333 @@
1
+ """
2
+ Engram - Core memory model for REM.
3
+
4
+ Engrams are structured memory documents that represent captured experiences,
5
+ observations, or insights. They are fundamentally Resources with optional
6
+ attached Moments, following the unified schema from the engram specification.
7
+
8
+ Key Design Principles:
9
+ - Engrams ARE Resources (category="engram")
10
+ - Human-friendly labels in graph edges (not UUIDs)
11
+ - Upsert with JSON merge behavior (never overwrite)
12
+ - Dual indexing: SQL + vector embeddings handled by repository
13
+ - YAML-first for human readability
14
+
15
+ Data Flow:
16
+ 1. Upload YAML/JSON engram (API or S3)
17
+ 2. Parse into Resource model
18
+ 3. Call repository.upsert() - automatically handles:
19
+ - SQL persistence
20
+ - Vector embedding generation
21
+ - Entity key index population
22
+ 4. Create attached Moments (if present)
23
+ 5. Link moments to parent engram via graph edges
24
+
25
+ Example Engram Structure:
26
+ ```yaml
27
+ kind: engram
28
+ name: "Daily Team Standup"
29
+ category: "meeting"
30
+ summary: "Daily standup discussing sprint progress"
31
+ timestamp: "2025-11-16T09:00:00Z"
32
+ uri: "s3://recordings/2025/11/16/standup.m4a"
33
+ content: |
34
+ Daily standup meeting with engineering team...
35
+
36
+ graph_edges:
37
+ - dst: "Q4 Roadmap Discussion"
38
+ rel_type: "semantic_similar"
39
+ weight: 0.75
40
+ properties:
41
+ dst_name: "Q4 Roadmap Discussion"
42
+ dst_entity_type: "resource/meeting"
43
+ confidence: 0.75
44
+
45
+ moments:
46
+ - name: "Sprint Progress Review"
47
+ content: "Sarah reviewed completed tickets"
48
+ summary: "Sprint progress update from Sarah"
49
+ starts_timestamp: "2025-11-16T09:00:00Z"
50
+ ends_timestamp: "2025-11-16T09:05:00Z"
51
+ moment_type: "meeting"
52
+ emotion_tags: ["focused", "productive"]
53
+ topic_tags: ["sprint-progress", "velocity"]
54
+ present_persons:
55
+ - id: "sarah-chen"
56
+ name: "Sarah Chen"
57
+ role: "VP Engineering"
58
+ ```
59
+
60
+ Graph Edge Labels:
61
+ - Use natural, human-friendly labels: "Sarah Chen", "Q4 Roadmap"
62
+ - NOT kebab-case unless it's a file path: "docs/api-spec.md"
63
+ - NOT UUIDs: "550e8400-e29b-41d4-a716-446655440000"
64
+ - Enables conversational queries: "LOOKUP Sarah Chen"
65
+
66
+ JSON Merge Behavior:
67
+ - Graph edges are MERGED, not replaced
68
+ - Metadata is MERGED, not replaced
69
+ - Arrays (tags) are MERGED and deduplicated
70
+ - Content/summary updated if provided
71
+ - Timestamps preserved if not provided
72
+
73
+ Best Practices:
74
+ - Use natural, descriptive names
75
+ - Include device metadata when available
76
+ - Use standard categories: diary, meeting, note, observation, conversation, media
77
+ - Attach moments for temporal segments
78
+ - Use appropriate relationship types in graph edges
79
+ - Always include timestamps in ISO 8601 format
80
+ """
81
+
82
+ from datetime import datetime, timezone
83
+ from typing import Optional
84
+
85
+ from pydantic import BaseModel, ConfigDict, Field
86
+
87
+ from .inline_edge import InlineEdge
88
+
89
+
90
+ class DeviceMetadata(BaseModel):
91
+ """Device metadata for engram capture context."""
92
+
93
+ imei: Optional[str] = Field(
94
+ default=None,
95
+ description="Device IMEI identifier",
96
+ )
97
+ model: Optional[str] = Field(
98
+ default=None,
99
+ description="Device model (e.g., 'iPhone 15 Pro', 'MacBook Pro')",
100
+ )
101
+ os: Optional[str] = Field(
102
+ default=None,
103
+ description="Operating system (e.g., 'iOS 18.1', 'macOS 14.2')",
104
+ )
105
+ app: Optional[str] = Field(
106
+ default=None,
107
+ description="Application name (e.g., 'Percolate Voice', 'Percolate Desktop')",
108
+ )
109
+ version: Optional[str] = Field(
110
+ default=None,
111
+ description="Application version",
112
+ )
113
+ location: Optional[dict] = Field(
114
+ default=None,
115
+ description="GPS location data (latitude, longitude, accuracy, altitude, etc.)",
116
+ )
117
+ network: Optional[dict] = Field(
118
+ default=None,
119
+ description="Network information (type, carrier, signal_strength)",
120
+ )
121
+
122
+
123
+ class EngramMoment(BaseModel):
124
+ """
125
+ Moment attached to an engram.
126
+
127
+ Represents a temporal segment within an engram with specific
128
+ temporal boundaries, present persons, and contextual metadata.
129
+ """
130
+
131
+ name: str = Field(
132
+ ...,
133
+ description="Moment name (human-readable)",
134
+ )
135
+ content: str = Field(
136
+ ...,
137
+ description="Moment content/description",
138
+ )
139
+ summary: Optional[str] = Field(
140
+ default=None,
141
+ description="Brief summary of the moment",
142
+ )
143
+ moment_type: Optional[str] = Field(
144
+ default=None,
145
+ description="Moment type (meeting, conversation, reflection, etc.)",
146
+ )
147
+ category: Optional[str] = Field(
148
+ default=None,
149
+ description="Moment category for grouping",
150
+ )
151
+ uri: Optional[str] = Field(
152
+ default=None,
153
+ description="Source URI (can include time fragment, e.g., 's3://file.m4a#t=0,300')",
154
+ )
155
+ starts_timestamp: Optional[datetime] = Field(
156
+ default=None,
157
+ description="Moment start time",
158
+ )
159
+ ends_timestamp: Optional[datetime] = Field(
160
+ default=None,
161
+ description="Moment end time",
162
+ )
163
+ emotion_tags: list[str] = Field(
164
+ default_factory=list,
165
+ description="Emotional context tags (focused, excited, concerned, etc.)",
166
+ )
167
+ topic_tags: list[str] = Field(
168
+ default_factory=list,
169
+ description="Topic tags in kebab-case (sprint-progress, api-design, etc.)",
170
+ )
171
+ present_persons: list[dict] = Field(
172
+ default_factory=list,
173
+ description="People present (Person objects with id, name, role)",
174
+ )
175
+ speakers: Optional[list[dict]] = Field(
176
+ default=None,
177
+ description="Speaker segments with text, speaker_identifier, timestamp, emotion",
178
+ )
179
+ location: Optional[str] = Field(
180
+ default=None,
181
+ description="GPS coordinates or descriptive location",
182
+ )
183
+ background_sounds: Optional[str] = Field(
184
+ default=None,
185
+ description="Ambient sounds description",
186
+ )
187
+ metadata: dict = Field(
188
+ default_factory=dict,
189
+ description="Additional moment metadata",
190
+ )
191
+ graph_edges: list[InlineEdge] = Field(
192
+ default_factory=list,
193
+ description="Knowledge graph edges for this moment",
194
+ )
195
+
196
+
197
+ class Engram(BaseModel):
198
+ """
199
+ Structured memory document for REM.
200
+
201
+ Engrams are Resources with category="engram", optionally containing
202
+ attached Moments. They represent captured experiences, observations,
203
+ or insights with rich contextual metadata.
204
+
205
+ Processing:
206
+ - Upsert as Resource via repository.upsert()
207
+ - Create attached Moments as separate entities
208
+ - Link moments to parent via graph edges (rel_type="part_of")
209
+
210
+ Chunking:
211
+ - Summary chunking (if summary exists)
212
+ - Content chunking (semantic-text-splitter for long content)
213
+ - Moment chunking (each moment is a separate chunk)
214
+
215
+ Embeddings:
216
+ - Generated for resource summary
217
+ - Generated for resource content (chunked if needed)
218
+ - Generated for each attached moment content
219
+ """
220
+
221
+ kind: str = Field(
222
+ default="engram",
223
+ description="Resource kind (always 'engram')",
224
+ )
225
+ name: str = Field(
226
+ ...,
227
+ description="Engram name (human-readable, used as graph label)",
228
+ )
229
+ category: str = Field(
230
+ default="engram",
231
+ description="Engram category (diary, meeting, note, observation, conversation, media)",
232
+ )
233
+ summary: Optional[str] = Field(
234
+ default=None,
235
+ description="Brief summary for semantic search (1-3 sentences)",
236
+ )
237
+ content: str = Field(
238
+ default="",
239
+ description="Full engram content",
240
+ )
241
+ uri: Optional[str] = Field(
242
+ default=None,
243
+ description="Resource URI (s3://, seaweedfs://, etc.)",
244
+ )
245
+ timestamp: datetime = Field(
246
+ default_factory=lambda: datetime.now(timezone.utc).replace(tzinfo=None),
247
+ description="Engram timestamp (content creation time)",
248
+ )
249
+ metadata: dict = Field(
250
+ default_factory=dict,
251
+ description="Engram metadata (device info, etc.)",
252
+ )
253
+ graph_edges: list[InlineEdge] = Field(
254
+ default_factory=list,
255
+ description="Knowledge graph edges",
256
+ )
257
+ moments: list[EngramMoment] = Field(
258
+ default_factory=list,
259
+ description="Attached moments (temporal segments)",
260
+ )
261
+
262
+ @property
263
+ def device(self) -> Optional[DeviceMetadata]:
264
+ """Get device metadata if present."""
265
+ device_data = self.metadata.get("device")
266
+ if device_data:
267
+ return DeviceMetadata(**device_data)
268
+ return None
269
+
270
+ def add_moment(
271
+ self,
272
+ name: str,
273
+ content: str,
274
+ summary: Optional[str] = None,
275
+ moment_type: Optional[str] = None,
276
+ starts_timestamp: Optional[datetime] = None,
277
+ ends_timestamp: Optional[datetime] = None,
278
+ **kwargs,
279
+ ) -> EngramMoment:
280
+ """
281
+ Add a moment to this engram.
282
+
283
+ Args:
284
+ name: Moment name
285
+ content: Moment content
286
+ summary: Brief summary
287
+ moment_type: Moment type
288
+ starts_timestamp: Start time
289
+ ends_timestamp: End time
290
+ **kwargs: Additional moment fields
291
+
292
+ Returns:
293
+ Created EngramMoment
294
+ """
295
+ moment = EngramMoment(
296
+ name=name,
297
+ content=content,
298
+ summary=summary,
299
+ moment_type=moment_type,
300
+ starts_timestamp=starts_timestamp,
301
+ ends_timestamp=ends_timestamp,
302
+ **kwargs,
303
+ )
304
+ self.moments.append(moment)
305
+ return moment
306
+
307
+ def add_graph_edge(
308
+ self,
309
+ dst: str,
310
+ rel_type: str,
311
+ weight: float = 0.5,
312
+ properties: Optional[dict] = None,
313
+ ) -> InlineEdge:
314
+ """
315
+ Add a graph edge to this engram.
316
+
317
+ Args:
318
+ dst: Human-friendly destination label
319
+ rel_type: Relationship type
320
+ weight: Edge weight (0.0-1.0)
321
+ properties: Edge properties
322
+
323
+ Returns:
324
+ Created InlineEdge
325
+ """
326
+ edge = InlineEdge(
327
+ dst=dst,
328
+ rel_type=rel_type,
329
+ weight=weight,
330
+ properties=properties or {},
331
+ )
332
+ self.graph_edges.append(edge)
333
+ return edge