runtime-memory 3.0.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.
- runtime_memory/__init__.py +28 -0
- runtime_memory/claude_code/__init__.py +48 -0
- runtime_memory/claude_code/commands.py +698 -0
- runtime_memory/claude_code/daemon.py +852 -0
- runtime_memory/claude_code/hooks.py +722 -0
- runtime_memory/cli/__init__.py +8 -0
- runtime_memory/cli/main.py +1936 -0
- runtime_memory/core/__init__.py +216 -0
- runtime_memory/core/config.py +473 -0
- runtime_memory/core/embeddings.py +908 -0
- runtime_memory/core/engine.py +1007 -0
- runtime_memory/core/exceptions.py +547 -0
- runtime_memory/core/legacy_env.py +39 -0
- runtime_memory/core/logging.py +160 -0
- runtime_memory/core/models.py +1051 -0
- runtime_memory/core/observability.py +725 -0
- runtime_memory/core/paths.py +30 -0
- runtime_memory/core/resilience.py +511 -0
- runtime_memory/core/retrieval.py +819 -0
- runtime_memory/core/storage.py +1105 -0
- runtime_memory/extraction/__init__.py +36 -0
- runtime_memory/extraction/extractor.py +1143 -0
- runtime_memory/hermes/__init__.py +39 -0
- runtime_memory/hermes/_base.py +154 -0
- runtime_memory/hermes/bridge.py +119 -0
- runtime_memory/hermes/plugin.yaml +13 -0
- runtime_memory/hermes/provider.py +536 -0
- runtime_memory/hermes/tools.py +230 -0
- runtime_memory/hermes/trace.py +177 -0
- runtime_memory/plugin/__init__.py +646 -0
- runtime_memory/sdk/__init__.py +97 -0
- runtime_memory/sdk/client.py +1577 -0
- runtime_memory/server/__init__.py +75 -0
- runtime_memory/server/api.py +1665 -0
- runtime_memory/server/mcp.py +1574 -0
- runtime_memory/server/static/css/styles.css +1110 -0
- runtime_memory/server/static/index.html +264 -0
- runtime_memory/server/static/js/api.js +294 -0
- runtime_memory/server/static/js/app.js +771 -0
- runtime_memory/tasks/__init__.py +114 -0
- runtime_memory/tasks/adapter.py +501 -0
- runtime_memory/tasks/claude_code_adapter.py +495 -0
- runtime_memory/tasks/claude_code_parser.py +339 -0
- runtime_memory/tasks/cli_bridge.py +415 -0
- runtime_memory/tasks/linking.py +397 -0
- runtime_memory/tasks/models.py +520 -0
- runtime_memory/tasks/outcomes.py +320 -0
- runtime_memory/tasks/parser.py +305 -0
- runtime_memory/tasks/unified_adapter.py +661 -0
- runtime_memory-3.0.0.dist-info/METADATA +497 -0
- runtime_memory-3.0.0.dist-info/RECORD +54 -0
- runtime_memory-3.0.0.dist-info/WHEEL +4 -0
- runtime_memory-3.0.0.dist-info/entry_points.txt +6 -0
- runtime_memory-3.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,1051 @@
|
|
|
1
|
+
"""Data models for Runtime Memory.
|
|
2
|
+
|
|
3
|
+
This module defines the core data structures:
|
|
4
|
+
- Enums for categories, scopes, sources, and outcomes
|
|
5
|
+
- Dataclasses for Memory, SearchResult, and ContextResponse
|
|
6
|
+
- Pydantic models for validation
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import uuid
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from datetime import UTC, datetime
|
|
14
|
+
from enum import Enum
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class MemoryCategory(str, Enum):
|
|
21
|
+
"""Categories for memory classification.
|
|
22
|
+
|
|
23
|
+
Each category represents a different type of knowledge that can be stored.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
# Project Knowledge
|
|
27
|
+
ARCHITECTURE = "architecture"
|
|
28
|
+
"""System design decisions (microservices, database choices)."""
|
|
29
|
+
|
|
30
|
+
CONVENTION = "convention"
|
|
31
|
+
"""Coding standards (naming, formatting, style)."""
|
|
32
|
+
|
|
33
|
+
DECISION = "decision"
|
|
34
|
+
"""Why something was built this way (rationale)."""
|
|
35
|
+
|
|
36
|
+
DEPENDENCY = "dependency"
|
|
37
|
+
"""What relies on what (v2 addition)."""
|
|
38
|
+
|
|
39
|
+
# Implementation Knowledge
|
|
40
|
+
PATTERN = "pattern"
|
|
41
|
+
"""Reusable code patterns (repository, factory)."""
|
|
42
|
+
|
|
43
|
+
GOTCHA = "gotcha"
|
|
44
|
+
"""Non-obvious behaviors and traps to avoid."""
|
|
45
|
+
|
|
46
|
+
WORKAROUND = "workaround"
|
|
47
|
+
"""Temporary fixes (with context on why)."""
|
|
48
|
+
|
|
49
|
+
# Operational Knowledge
|
|
50
|
+
TROUBLESHOOTING = "troubleshooting"
|
|
51
|
+
"""Error → solution mappings."""
|
|
52
|
+
|
|
53
|
+
COMMAND = "command"
|
|
54
|
+
"""Useful shell/npm/etc. commands."""
|
|
55
|
+
|
|
56
|
+
ENVIRONMENT = "environment"
|
|
57
|
+
"""Setup, config, secrets locations (v2 addition)."""
|
|
58
|
+
|
|
59
|
+
# User Preferences
|
|
60
|
+
PREFERENCE = "preference"
|
|
61
|
+
"""User preferences (style, tools)."""
|
|
62
|
+
|
|
63
|
+
CODING_STYLE = "coding_style"
|
|
64
|
+
"""Tabs vs spaces, naming conventions (v2 addition)."""
|
|
65
|
+
|
|
66
|
+
TOOL_PREFERENCE = "tool_preference"
|
|
67
|
+
"""Preferred libraries, frameworks (v2 addition)."""
|
|
68
|
+
|
|
69
|
+
# Meta
|
|
70
|
+
CONTEXT = "context"
|
|
71
|
+
"""Current work state (v2 addition)."""
|
|
72
|
+
|
|
73
|
+
TODO = "todo"
|
|
74
|
+
"""Pending items (v2 addition)."""
|
|
75
|
+
|
|
76
|
+
# General
|
|
77
|
+
GENERAL = "general"
|
|
78
|
+
"""Uncategorized memories (v2 addition)."""
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class MemoryScope(str, Enum):
|
|
82
|
+
"""Scope levels for memory visibility.
|
|
83
|
+
|
|
84
|
+
Determines where a memory is applicable.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
GLOBAL = "global"
|
|
88
|
+
"""Available across all projects."""
|
|
89
|
+
|
|
90
|
+
PROJECT = "project"
|
|
91
|
+
"""Specific to a single project."""
|
|
92
|
+
|
|
93
|
+
SESSION = "session"
|
|
94
|
+
"""Only valid for the current session."""
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class MemorySource(str, Enum):
|
|
98
|
+
"""Source of the memory.
|
|
99
|
+
|
|
100
|
+
Indicates how the memory was created.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
EXPLICIT = "explicit"
|
|
104
|
+
"""Manually added by user via /remember or API."""
|
|
105
|
+
|
|
106
|
+
EXTRACTED = "extracted"
|
|
107
|
+
"""Automatically extracted from conversation."""
|
|
108
|
+
|
|
109
|
+
IMPORTED = "imported"
|
|
110
|
+
"""Imported from external source (file, task system)."""
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class Outcome(str, Enum):
|
|
114
|
+
"""Outcome feedback for a memory.
|
|
115
|
+
|
|
116
|
+
Used to adjust the memory's outcome_score.
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
WORKED = "worked"
|
|
120
|
+
"""Advice solved the problem. Score adjustment: +0.2"""
|
|
121
|
+
|
|
122
|
+
FAILED = "failed"
|
|
123
|
+
"""Advice was wrong. Score adjustment: -0.3"""
|
|
124
|
+
|
|
125
|
+
PARTIAL = "partial"
|
|
126
|
+
"""Advice was on the right track but incomplete. Score adjustment: +0.05"""
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# Score adjustments for each outcome
|
|
130
|
+
OUTCOME_SCORE_ADJUSTMENTS: dict[Outcome, float] = {
|
|
131
|
+
Outcome.WORKED: 0.2,
|
|
132
|
+
Outcome.FAILED: -0.3,
|
|
133
|
+
Outcome.PARTIAL: 0.05,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class RelationType(str, Enum):
|
|
138
|
+
"""Types of relationships between memories (v2 addition).
|
|
139
|
+
|
|
140
|
+
Used to track how memories relate to each other.
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
UPDATES = "updates"
|
|
144
|
+
"""New info replaces old (e.g., new decision supersedes old)."""
|
|
145
|
+
|
|
146
|
+
EXTENDS = "extends"
|
|
147
|
+
"""Enriches without replacing (e.g., adds detail to existing memory)."""
|
|
148
|
+
|
|
149
|
+
DERIVES = "derives"
|
|
150
|
+
"""Inferred connection (e.g., pattern derived from multiple examples)."""
|
|
151
|
+
|
|
152
|
+
RELATES_TO = "relates_to"
|
|
153
|
+
"""Soft connection (e.g., same topic, related concepts)."""
|
|
154
|
+
|
|
155
|
+
CONFLICTS_WITH = "conflicts_with"
|
|
156
|
+
"""Contradictory information (requires resolution)."""
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class EntityType(str, Enum):
|
|
160
|
+
"""Types of entities that can be extracted from memories (v2 addition)."""
|
|
161
|
+
|
|
162
|
+
FILE = "file"
|
|
163
|
+
"""A file path or file name."""
|
|
164
|
+
|
|
165
|
+
MODULE = "module"
|
|
166
|
+
"""A code module or package."""
|
|
167
|
+
|
|
168
|
+
FUNCTION = "function"
|
|
169
|
+
"""A function or method name."""
|
|
170
|
+
|
|
171
|
+
CLASS = "class"
|
|
172
|
+
"""A class name."""
|
|
173
|
+
|
|
174
|
+
VARIABLE = "variable"
|
|
175
|
+
"""A variable or constant name."""
|
|
176
|
+
|
|
177
|
+
ERROR = "error"
|
|
178
|
+
"""An error message or error type."""
|
|
179
|
+
|
|
180
|
+
CONCEPT = "concept"
|
|
181
|
+
"""An abstract concept or pattern name."""
|
|
182
|
+
|
|
183
|
+
TOOL = "tool"
|
|
184
|
+
"""A tool, library, or framework."""
|
|
185
|
+
|
|
186
|
+
PERSON = "person"
|
|
187
|
+
"""A person's name (e.g., team member)."""
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def generate_id() -> str:
|
|
191
|
+
"""Generate a unique memory ID.
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
UUID string.
|
|
195
|
+
"""
|
|
196
|
+
return str(uuid.uuid4())
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def utc_now() -> datetime:
|
|
200
|
+
"""Get current UTC timestamp.
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
Current datetime in UTC.
|
|
204
|
+
"""
|
|
205
|
+
return datetime.now(UTC)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@dataclass
|
|
209
|
+
class Memory:
|
|
210
|
+
"""Core memory data structure.
|
|
211
|
+
|
|
212
|
+
Represents a single piece of stored knowledge with metadata
|
|
213
|
+
for retrieval and outcome-based learning.
|
|
214
|
+
"""
|
|
215
|
+
|
|
216
|
+
content: str
|
|
217
|
+
"""The memory content text."""
|
|
218
|
+
|
|
219
|
+
category: MemoryCategory
|
|
220
|
+
"""Classification category."""
|
|
221
|
+
|
|
222
|
+
id: str = field(default_factory=generate_id)
|
|
223
|
+
"""Unique identifier (UUID)."""
|
|
224
|
+
|
|
225
|
+
outcome_score: float = 0.0
|
|
226
|
+
"""Accumulated outcome score (-1.0 to 1.0)."""
|
|
227
|
+
|
|
228
|
+
confidence: float = 1.0
|
|
229
|
+
"""Source reliability (0.0 to 1.0)."""
|
|
230
|
+
|
|
231
|
+
importance: float = 0.5
|
|
232
|
+
"""Importance weight (0.0 to 1.0)."""
|
|
233
|
+
|
|
234
|
+
use_count: int = 0
|
|
235
|
+
"""Times retrieved in search."""
|
|
236
|
+
|
|
237
|
+
project: str | None = None
|
|
238
|
+
"""Project scope (None for global)."""
|
|
239
|
+
|
|
240
|
+
scope: MemoryScope = MemoryScope.PROJECT
|
|
241
|
+
"""Visibility scope."""
|
|
242
|
+
|
|
243
|
+
source: MemorySource = MemorySource.EXPLICIT
|
|
244
|
+
"""How the memory was created."""
|
|
245
|
+
|
|
246
|
+
tags: list[str] = field(default_factory=list)
|
|
247
|
+
"""Optional tags for additional categorization."""
|
|
248
|
+
|
|
249
|
+
entities: list[str] = field(default_factory=list)
|
|
250
|
+
"""Detected entities (files, functions, errors)."""
|
|
251
|
+
|
|
252
|
+
supersedes: str | None = None
|
|
253
|
+
"""ID of memory this one replaces."""
|
|
254
|
+
|
|
255
|
+
archived: bool = False
|
|
256
|
+
"""Whether the memory is archived (soft deleted)."""
|
|
257
|
+
|
|
258
|
+
created_at: datetime = field(default_factory=utc_now)
|
|
259
|
+
"""Creation timestamp."""
|
|
260
|
+
|
|
261
|
+
updated_at: datetime = field(default_factory=utc_now)
|
|
262
|
+
"""Last update timestamp."""
|
|
263
|
+
|
|
264
|
+
embedding: list[float] | None = None
|
|
265
|
+
"""Vector embedding for semantic search."""
|
|
266
|
+
|
|
267
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
268
|
+
"""Additional metadata."""
|
|
269
|
+
|
|
270
|
+
def apply_outcome(self, outcome: Outcome) -> None:
|
|
271
|
+
"""Apply an outcome to adjust the score.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
outcome: The outcome to apply.
|
|
275
|
+
"""
|
|
276
|
+
adjustment = OUTCOME_SCORE_ADJUSTMENTS[outcome]
|
|
277
|
+
self.outcome_score = max(-1.0, min(1.0, self.outcome_score + adjustment))
|
|
278
|
+
self.updated_at = utc_now()
|
|
279
|
+
|
|
280
|
+
def increment_use_count(self) -> None:
|
|
281
|
+
"""Increment the use count when memory is retrieved."""
|
|
282
|
+
self.use_count += 1
|
|
283
|
+
self.updated_at = utc_now()
|
|
284
|
+
|
|
285
|
+
def archive(self) -> None:
|
|
286
|
+
"""Archive the memory (soft delete)."""
|
|
287
|
+
self.archived = True
|
|
288
|
+
self.updated_at = utc_now()
|
|
289
|
+
|
|
290
|
+
def to_dict(self) -> dict[str, Any]:
|
|
291
|
+
"""Convert to dictionary representation.
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
Dictionary with all fields.
|
|
295
|
+
"""
|
|
296
|
+
return {
|
|
297
|
+
"id": self.id,
|
|
298
|
+
"content": self.content,
|
|
299
|
+
"category": self.category.value,
|
|
300
|
+
"outcome_score": self.outcome_score,
|
|
301
|
+
"confidence": self.confidence,
|
|
302
|
+
"importance": self.importance,
|
|
303
|
+
"use_count": self.use_count,
|
|
304
|
+
"project": self.project,
|
|
305
|
+
"scope": self.scope.value,
|
|
306
|
+
"source": self.source.value,
|
|
307
|
+
"tags": self.tags,
|
|
308
|
+
"entities": self.entities,
|
|
309
|
+
"supersedes": self.supersedes,
|
|
310
|
+
"archived": self.archived,
|
|
311
|
+
"created_at": self.created_at.isoformat(),
|
|
312
|
+
"updated_at": self.updated_at.isoformat(),
|
|
313
|
+
"embedding": self.embedding,
|
|
314
|
+
"metadata": self.metadata,
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
@classmethod
|
|
318
|
+
def from_dict(cls, data: dict[str, Any]) -> Memory:
|
|
319
|
+
"""Create Memory from dictionary.
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
data: Dictionary with memory fields.
|
|
323
|
+
|
|
324
|
+
Returns:
|
|
325
|
+
Memory instance.
|
|
326
|
+
"""
|
|
327
|
+
# Handle datetime conversion
|
|
328
|
+
created_at = data.get("created_at")
|
|
329
|
+
if isinstance(created_at, str):
|
|
330
|
+
created_at = datetime.fromisoformat(created_at)
|
|
331
|
+
elif created_at is None:
|
|
332
|
+
created_at = utc_now()
|
|
333
|
+
|
|
334
|
+
updated_at = data.get("updated_at")
|
|
335
|
+
if isinstance(updated_at, str):
|
|
336
|
+
updated_at = datetime.fromisoformat(updated_at)
|
|
337
|
+
elif updated_at is None:
|
|
338
|
+
updated_at = utc_now()
|
|
339
|
+
|
|
340
|
+
# Handle enum conversion
|
|
341
|
+
category_raw = data.get("category")
|
|
342
|
+
if isinstance(category_raw, str):
|
|
343
|
+
category = MemoryCategory(category_raw)
|
|
344
|
+
elif isinstance(category_raw, MemoryCategory):
|
|
345
|
+
category = category_raw
|
|
346
|
+
else:
|
|
347
|
+
raise ValueError("category is required and must be a valid MemoryCategory")
|
|
348
|
+
|
|
349
|
+
scope = data.get("scope", MemoryScope.PROJECT)
|
|
350
|
+
if isinstance(scope, str):
|
|
351
|
+
scope = MemoryScope(scope)
|
|
352
|
+
|
|
353
|
+
source = data.get("source", MemorySource.EXPLICIT)
|
|
354
|
+
if isinstance(source, str):
|
|
355
|
+
source = MemorySource(source)
|
|
356
|
+
|
|
357
|
+
return cls(
|
|
358
|
+
id=data.get("id", generate_id()),
|
|
359
|
+
content=data["content"],
|
|
360
|
+
category=category,
|
|
361
|
+
outcome_score=data.get("outcome_score", 0.0),
|
|
362
|
+
confidence=data.get("confidence", 1.0),
|
|
363
|
+
importance=data.get("importance", 0.5),
|
|
364
|
+
use_count=data.get("use_count", 0),
|
|
365
|
+
project=data.get("project"),
|
|
366
|
+
scope=scope,
|
|
367
|
+
source=source,
|
|
368
|
+
tags=data.get("tags", []),
|
|
369
|
+
entities=data.get("entities", []),
|
|
370
|
+
supersedes=data.get("supersedes"),
|
|
371
|
+
archived=data.get("archived", False),
|
|
372
|
+
created_at=created_at,
|
|
373
|
+
updated_at=updated_at,
|
|
374
|
+
embedding=data.get("embedding"),
|
|
375
|
+
metadata=data.get("metadata", {}),
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
@dataclass
|
|
380
|
+
class SearchResult:
|
|
381
|
+
"""Result from a memory search operation.
|
|
382
|
+
|
|
383
|
+
Contains the memory along with relevance scoring details.
|
|
384
|
+
"""
|
|
385
|
+
|
|
386
|
+
memory: Memory
|
|
387
|
+
"""The matched memory."""
|
|
388
|
+
|
|
389
|
+
score: float
|
|
390
|
+
"""Final combined relevance score."""
|
|
391
|
+
|
|
392
|
+
semantic_score: float = 0.0
|
|
393
|
+
"""Semantic similarity component (BM25 + vector)."""
|
|
394
|
+
|
|
395
|
+
recency_score: float = 0.0
|
|
396
|
+
"""Recency decay component."""
|
|
397
|
+
|
|
398
|
+
frequency_score: float = 0.0
|
|
399
|
+
"""Usage frequency component."""
|
|
400
|
+
|
|
401
|
+
category_boost: float = 1.0
|
|
402
|
+
"""Category-specific boost factor."""
|
|
403
|
+
|
|
404
|
+
def to_dict(self) -> dict[str, Any]:
|
|
405
|
+
"""Convert to dictionary representation.
|
|
406
|
+
|
|
407
|
+
Returns:
|
|
408
|
+
Dictionary with all fields.
|
|
409
|
+
"""
|
|
410
|
+
return {
|
|
411
|
+
"memory": self.memory.to_dict(),
|
|
412
|
+
"score": self.score,
|
|
413
|
+
"semantic_score": self.semantic_score,
|
|
414
|
+
"recency_score": self.recency_score,
|
|
415
|
+
"frequency_score": self.frequency_score,
|
|
416
|
+
"category_boost": self.category_boost,
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
@dataclass
|
|
421
|
+
class ContextResponse:
|
|
422
|
+
"""Response containing formatted context for injection.
|
|
423
|
+
|
|
424
|
+
Used to provide relevant memories as context to an AI agent.
|
|
425
|
+
"""
|
|
426
|
+
|
|
427
|
+
memories: list[Memory]
|
|
428
|
+
"""List of relevant memories."""
|
|
429
|
+
|
|
430
|
+
project: str | None
|
|
431
|
+
"""Project scope for the context."""
|
|
432
|
+
|
|
433
|
+
total_count: int
|
|
434
|
+
"""Total number of memories available."""
|
|
435
|
+
|
|
436
|
+
included_count: int
|
|
437
|
+
"""Number of memories included in response."""
|
|
438
|
+
|
|
439
|
+
formatted: str = ""
|
|
440
|
+
"""Pre-formatted context string for injection."""
|
|
441
|
+
|
|
442
|
+
categories: dict[str, int] = field(default_factory=dict)
|
|
443
|
+
"""Count of memories by category."""
|
|
444
|
+
|
|
445
|
+
def to_markdown(self) -> str:
|
|
446
|
+
"""Format context as markdown for injection.
|
|
447
|
+
|
|
448
|
+
Returns:
|
|
449
|
+
Markdown-formatted context string.
|
|
450
|
+
"""
|
|
451
|
+
if not self.memories:
|
|
452
|
+
return "No relevant memories found."
|
|
453
|
+
|
|
454
|
+
lines = ["# Project Knowledge", ""]
|
|
455
|
+
|
|
456
|
+
# Group by category
|
|
457
|
+
by_category: dict[MemoryCategory, list[Memory]] = {}
|
|
458
|
+
for memory in self.memories:
|
|
459
|
+
if memory.category not in by_category:
|
|
460
|
+
by_category[memory.category] = []
|
|
461
|
+
by_category[memory.category].append(memory)
|
|
462
|
+
|
|
463
|
+
# Format each category
|
|
464
|
+
for category in MemoryCategory:
|
|
465
|
+
if category in by_category:
|
|
466
|
+
memories = by_category[category]
|
|
467
|
+
lines.append(f"## {category.value.title()}")
|
|
468
|
+
lines.append("")
|
|
469
|
+
for memory in memories:
|
|
470
|
+
score_indicator = ""
|
|
471
|
+
if memory.outcome_score > 0.3:
|
|
472
|
+
score_indicator = " [high confidence]"
|
|
473
|
+
elif memory.outcome_score < -0.3:
|
|
474
|
+
score_indicator = " [low confidence]"
|
|
475
|
+
lines.append(f"- {memory.content}{score_indicator}")
|
|
476
|
+
lines.append("")
|
|
477
|
+
|
|
478
|
+
return "\n".join(lines)
|
|
479
|
+
|
|
480
|
+
def to_dict(self) -> dict[str, Any]:
|
|
481
|
+
"""Convert to dictionary representation.
|
|
482
|
+
|
|
483
|
+
Returns:
|
|
484
|
+
Dictionary with all fields.
|
|
485
|
+
"""
|
|
486
|
+
return {
|
|
487
|
+
"memories": [m.to_dict() for m in self.memories],
|
|
488
|
+
"project": self.project,
|
|
489
|
+
"total_count": self.total_count,
|
|
490
|
+
"included_count": self.included_count,
|
|
491
|
+
"formatted": self.formatted or self.to_markdown(),
|
|
492
|
+
"categories": self.categories,
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
# =============================================================================
|
|
497
|
+
# V2 Model Additions: Relationships, Entities, Routing Patterns
|
|
498
|
+
# =============================================================================
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
@dataclass
|
|
502
|
+
class Relationship:
|
|
503
|
+
"""A relationship between two memories (v2 addition).
|
|
504
|
+
|
|
505
|
+
Tracks how memories relate to each other, enabling knowledge graph
|
|
506
|
+
capabilities and conflict detection.
|
|
507
|
+
"""
|
|
508
|
+
|
|
509
|
+
source_id: str
|
|
510
|
+
"""ID of the source memory."""
|
|
511
|
+
|
|
512
|
+
target_id: str
|
|
513
|
+
"""ID of the target memory."""
|
|
514
|
+
|
|
515
|
+
relation_type: RelationType
|
|
516
|
+
"""Type of relationship."""
|
|
517
|
+
|
|
518
|
+
strength: float = 1.0
|
|
519
|
+
"""Relationship strength (0.0 to 1.0)."""
|
|
520
|
+
|
|
521
|
+
created_at: datetime = field(default_factory=utc_now)
|
|
522
|
+
"""When the relationship was created."""
|
|
523
|
+
|
|
524
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
525
|
+
"""Additional relationship metadata."""
|
|
526
|
+
|
|
527
|
+
def to_dict(self) -> dict[str, Any]:
|
|
528
|
+
"""Convert to dictionary representation."""
|
|
529
|
+
return {
|
|
530
|
+
"source_id": self.source_id,
|
|
531
|
+
"target_id": self.target_id,
|
|
532
|
+
"relation_type": self.relation_type.value,
|
|
533
|
+
"strength": self.strength,
|
|
534
|
+
"created_at": self.created_at.isoformat(),
|
|
535
|
+
"metadata": self.metadata,
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
@classmethod
|
|
539
|
+
def from_dict(cls, data: dict[str, Any]) -> Relationship:
|
|
540
|
+
"""Create Relationship from dictionary."""
|
|
541
|
+
created_at = data.get("created_at")
|
|
542
|
+
if isinstance(created_at, str):
|
|
543
|
+
created_at = datetime.fromisoformat(created_at)
|
|
544
|
+
elif created_at is None:
|
|
545
|
+
created_at = utc_now()
|
|
546
|
+
|
|
547
|
+
return cls(
|
|
548
|
+
source_id=data["source_id"],
|
|
549
|
+
target_id=data["target_id"],
|
|
550
|
+
relation_type=RelationType(data["relation_type"]),
|
|
551
|
+
strength=data.get("strength", 1.0),
|
|
552
|
+
created_at=created_at,
|
|
553
|
+
metadata=data.get("metadata", {}),
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
@dataclass
|
|
558
|
+
class Entity:
|
|
559
|
+
"""An entity extracted from memories (v2 addition).
|
|
560
|
+
|
|
561
|
+
Represents a named entity (file, module, function, concept, etc.)
|
|
562
|
+
that can be referenced by multiple memories.
|
|
563
|
+
"""
|
|
564
|
+
|
|
565
|
+
id: str = field(default_factory=generate_id)
|
|
566
|
+
"""Unique identifier for the entity."""
|
|
567
|
+
|
|
568
|
+
name: str = ""
|
|
569
|
+
"""The entity name (e.g., 'auth_service.py', 'UserRepository')."""
|
|
570
|
+
|
|
571
|
+
entity_type: EntityType = EntityType.CONCEPT
|
|
572
|
+
"""Type of entity."""
|
|
573
|
+
|
|
574
|
+
description: str = ""
|
|
575
|
+
"""Optional description of the entity."""
|
|
576
|
+
|
|
577
|
+
memory_ids: list[str] = field(default_factory=list)
|
|
578
|
+
"""IDs of memories that reference this entity."""
|
|
579
|
+
|
|
580
|
+
created_at: datetime = field(default_factory=utc_now)
|
|
581
|
+
"""When the entity was first detected."""
|
|
582
|
+
|
|
583
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
584
|
+
"""Additional entity metadata."""
|
|
585
|
+
|
|
586
|
+
def to_dict(self) -> dict[str, Any]:
|
|
587
|
+
"""Convert to dictionary representation."""
|
|
588
|
+
return {
|
|
589
|
+
"id": self.id,
|
|
590
|
+
"name": self.name,
|
|
591
|
+
"entity_type": self.entity_type.value,
|
|
592
|
+
"description": self.description,
|
|
593
|
+
"memory_ids": self.memory_ids,
|
|
594
|
+
"created_at": self.created_at.isoformat(),
|
|
595
|
+
"metadata": self.metadata,
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
@classmethod
|
|
599
|
+
def from_dict(cls, data: dict[str, Any]) -> Entity:
|
|
600
|
+
"""Create Entity from dictionary."""
|
|
601
|
+
created_at = data.get("created_at")
|
|
602
|
+
if isinstance(created_at, str):
|
|
603
|
+
created_at = datetime.fromisoformat(created_at)
|
|
604
|
+
elif created_at is None:
|
|
605
|
+
created_at = utc_now()
|
|
606
|
+
|
|
607
|
+
return cls(
|
|
608
|
+
id=data.get("id", generate_id()),
|
|
609
|
+
name=data.get("name", ""),
|
|
610
|
+
entity_type=EntityType(data.get("entity_type", "concept")),
|
|
611
|
+
description=data.get("description", ""),
|
|
612
|
+
memory_ids=data.get("memory_ids", []),
|
|
613
|
+
created_at=created_at,
|
|
614
|
+
metadata=data.get("metadata", {}),
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
@dataclass
|
|
619
|
+
class RoutingPattern:
|
|
620
|
+
"""Learned query → category mapping (v2 addition).
|
|
621
|
+
|
|
622
|
+
Inspired by Roampal's Routing KG, this tracks which query patterns
|
|
623
|
+
lead to which categories being most useful.
|
|
624
|
+
"""
|
|
625
|
+
|
|
626
|
+
id: str = field(default_factory=generate_id)
|
|
627
|
+
"""Unique identifier."""
|
|
628
|
+
|
|
629
|
+
query_pattern: str = ""
|
|
630
|
+
"""The query pattern (keywords or regex)."""
|
|
631
|
+
|
|
632
|
+
category: MemoryCategory = MemoryCategory.GENERAL
|
|
633
|
+
"""Target category for this pattern."""
|
|
634
|
+
|
|
635
|
+
success_count: int = 0
|
|
636
|
+
"""Times this routing led to a successful outcome."""
|
|
637
|
+
|
|
638
|
+
fail_count: int = 0
|
|
639
|
+
"""Times this routing led to a failed outcome."""
|
|
640
|
+
|
|
641
|
+
created_at: datetime = field(default_factory=utc_now)
|
|
642
|
+
"""When the pattern was first detected."""
|
|
643
|
+
|
|
644
|
+
updated_at: datetime = field(default_factory=utc_now)
|
|
645
|
+
"""When the pattern was last updated."""
|
|
646
|
+
|
|
647
|
+
@property
|
|
648
|
+
def success_rate(self) -> float:
|
|
649
|
+
"""Calculate the success rate for this routing pattern."""
|
|
650
|
+
total = self.success_count + self.fail_count
|
|
651
|
+
if total == 0:
|
|
652
|
+
return 0.5 # Neutral default
|
|
653
|
+
return self.success_count / total
|
|
654
|
+
|
|
655
|
+
@property
|
|
656
|
+
def confidence(self) -> float:
|
|
657
|
+
"""Calculate confidence in this routing pattern.
|
|
658
|
+
|
|
659
|
+
Higher confidence with more data points.
|
|
660
|
+
"""
|
|
661
|
+
total = self.success_count + self.fail_count
|
|
662
|
+
if total < 5:
|
|
663
|
+
return 0.3 # Low confidence with few samples
|
|
664
|
+
elif total < 20:
|
|
665
|
+
return 0.6 # Medium confidence
|
|
666
|
+
else:
|
|
667
|
+
return 0.9 # High confidence with many samples
|
|
668
|
+
|
|
669
|
+
def record_outcome(self, success: bool) -> None:
|
|
670
|
+
"""Record an outcome for this routing pattern.
|
|
671
|
+
|
|
672
|
+
Args:
|
|
673
|
+
success: Whether the routing led to a successful outcome.
|
|
674
|
+
"""
|
|
675
|
+
if success:
|
|
676
|
+
self.success_count += 1
|
|
677
|
+
else:
|
|
678
|
+
self.fail_count += 1
|
|
679
|
+
self.updated_at = utc_now()
|
|
680
|
+
|
|
681
|
+
def to_dict(self) -> dict[str, Any]:
|
|
682
|
+
"""Convert to dictionary representation."""
|
|
683
|
+
return {
|
|
684
|
+
"id": self.id,
|
|
685
|
+
"query_pattern": self.query_pattern,
|
|
686
|
+
"category": self.category.value,
|
|
687
|
+
"success_count": self.success_count,
|
|
688
|
+
"fail_count": self.fail_count,
|
|
689
|
+
"success_rate": self.success_rate,
|
|
690
|
+
"confidence": self.confidence,
|
|
691
|
+
"created_at": self.created_at.isoformat(),
|
|
692
|
+
"updated_at": self.updated_at.isoformat(),
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
@classmethod
|
|
696
|
+
def from_dict(cls, data: dict[str, Any]) -> RoutingPattern:
|
|
697
|
+
"""Create RoutingPattern from dictionary."""
|
|
698
|
+
created_at = data.get("created_at")
|
|
699
|
+
if isinstance(created_at, str):
|
|
700
|
+
created_at = datetime.fromisoformat(created_at)
|
|
701
|
+
elif created_at is None:
|
|
702
|
+
created_at = utc_now()
|
|
703
|
+
|
|
704
|
+
updated_at = data.get("updated_at")
|
|
705
|
+
if isinstance(updated_at, str):
|
|
706
|
+
updated_at = datetime.fromisoformat(updated_at)
|
|
707
|
+
elif updated_at is None:
|
|
708
|
+
updated_at = utc_now()
|
|
709
|
+
|
|
710
|
+
return cls(
|
|
711
|
+
id=data.get("id", generate_id()),
|
|
712
|
+
query_pattern=data.get("query_pattern", ""),
|
|
713
|
+
category=MemoryCategory(data.get("category", "general")),
|
|
714
|
+
success_count=data.get("success_count", 0),
|
|
715
|
+
fail_count=data.get("fail_count", 0),
|
|
716
|
+
created_at=created_at,
|
|
717
|
+
updated_at=updated_at,
|
|
718
|
+
)
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
# =============================================================================
|
|
722
|
+
# Pydantic Validation Models
|
|
723
|
+
# =============================================================================
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
class MemoryCreate(BaseModel):
|
|
727
|
+
"""Pydantic model for creating a new memory."""
|
|
728
|
+
|
|
729
|
+
model_config = ConfigDict(str_strip_whitespace=True)
|
|
730
|
+
|
|
731
|
+
content: str = Field(..., min_length=1, max_length=100000)
|
|
732
|
+
"""The memory content (required, 1-100000 chars)."""
|
|
733
|
+
|
|
734
|
+
category: MemoryCategory
|
|
735
|
+
"""Classification category (required)."""
|
|
736
|
+
|
|
737
|
+
project: str | None = Field(default=None, max_length=255)
|
|
738
|
+
"""Project scope (optional)."""
|
|
739
|
+
|
|
740
|
+
scope: MemoryScope = MemoryScope.PROJECT
|
|
741
|
+
"""Visibility scope."""
|
|
742
|
+
|
|
743
|
+
source: MemorySource = MemorySource.EXPLICIT
|
|
744
|
+
"""How the memory was created."""
|
|
745
|
+
|
|
746
|
+
confidence: float = Field(default=1.0, ge=0.0, le=1.0)
|
|
747
|
+
"""Source reliability (0.0-1.0)."""
|
|
748
|
+
|
|
749
|
+
importance: float = Field(default=0.5, ge=0.0, le=1.0)
|
|
750
|
+
"""Importance weight (0.0-1.0)."""
|
|
751
|
+
|
|
752
|
+
tags: list[str] = Field(default_factory=list, max_length=20)
|
|
753
|
+
"""Optional tags (max 20)."""
|
|
754
|
+
|
|
755
|
+
entities: list[str] = Field(default_factory=list, max_length=50)
|
|
756
|
+
"""Detected entities (max 50)."""
|
|
757
|
+
|
|
758
|
+
supersedes: str | None = Field(default=None, max_length=36)
|
|
759
|
+
"""ID of memory this one replaces."""
|
|
760
|
+
|
|
761
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
762
|
+
"""Additional metadata."""
|
|
763
|
+
|
|
764
|
+
@field_validator("content")
|
|
765
|
+
@classmethod
|
|
766
|
+
def content_not_empty(cls, v: str) -> str:
|
|
767
|
+
"""Validate content is not just whitespace."""
|
|
768
|
+
if not v.strip():
|
|
769
|
+
raise ValueError("Content cannot be empty or whitespace only")
|
|
770
|
+
return v.strip()
|
|
771
|
+
|
|
772
|
+
@field_validator("tags")
|
|
773
|
+
@classmethod
|
|
774
|
+
def validate_tags(cls, v: list[str]) -> list[str]:
|
|
775
|
+
"""Validate and normalize tags."""
|
|
776
|
+
return [tag.strip().lower() for tag in v if tag.strip()]
|
|
777
|
+
|
|
778
|
+
def to_memory(self) -> Memory:
|
|
779
|
+
"""Convert to Memory dataclass.
|
|
780
|
+
|
|
781
|
+
Returns:
|
|
782
|
+
Memory instance.
|
|
783
|
+
"""
|
|
784
|
+
return Memory(
|
|
785
|
+
content=self.content,
|
|
786
|
+
category=self.category,
|
|
787
|
+
project=self.project,
|
|
788
|
+
scope=self.scope,
|
|
789
|
+
source=self.source,
|
|
790
|
+
confidence=self.confidence,
|
|
791
|
+
importance=self.importance,
|
|
792
|
+
tags=self.tags,
|
|
793
|
+
entities=self.entities,
|
|
794
|
+
supersedes=self.supersedes,
|
|
795
|
+
metadata=self.metadata,
|
|
796
|
+
)
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
class MemoryUpdate(BaseModel):
|
|
800
|
+
"""Pydantic model for updating an existing memory."""
|
|
801
|
+
|
|
802
|
+
model_config = ConfigDict(str_strip_whitespace=True)
|
|
803
|
+
|
|
804
|
+
content: str | None = Field(default=None, min_length=1, max_length=100000)
|
|
805
|
+
"""New content (optional, 1-100000 chars)."""
|
|
806
|
+
|
|
807
|
+
category: MemoryCategory | None = None
|
|
808
|
+
"""New category (optional)."""
|
|
809
|
+
|
|
810
|
+
confidence: float | None = Field(default=None, ge=0.0, le=1.0)
|
|
811
|
+
"""New confidence (optional)."""
|
|
812
|
+
|
|
813
|
+
importance: float | None = Field(default=None, ge=0.0, le=1.0)
|
|
814
|
+
"""New importance (optional)."""
|
|
815
|
+
|
|
816
|
+
tags: list[str] | None = Field(default=None, max_length=20)
|
|
817
|
+
"""New tags (optional)."""
|
|
818
|
+
|
|
819
|
+
entities: list[str] | None = Field(default=None, max_length=50)
|
|
820
|
+
"""New entities (optional)."""
|
|
821
|
+
|
|
822
|
+
metadata: dict[str, Any] | None = None
|
|
823
|
+
"""New metadata (optional)."""
|
|
824
|
+
|
|
825
|
+
@field_validator("content")
|
|
826
|
+
@classmethod
|
|
827
|
+
def content_not_empty(cls, v: str | None) -> str | None:
|
|
828
|
+
"""Validate content is not just whitespace if provided."""
|
|
829
|
+
if v is not None and not v.strip():
|
|
830
|
+
raise ValueError("Content cannot be empty or whitespace only")
|
|
831
|
+
return v.strip() if v else None
|
|
832
|
+
|
|
833
|
+
@field_validator("tags")
|
|
834
|
+
@classmethod
|
|
835
|
+
def validate_tags(cls, v: list[str] | None) -> list[str] | None:
|
|
836
|
+
"""Validate and normalize tags."""
|
|
837
|
+
if v is None:
|
|
838
|
+
return None
|
|
839
|
+
return [tag.strip().lower() for tag in v if tag.strip()]
|
|
840
|
+
|
|
841
|
+
def apply_to(self, memory: Memory) -> Memory:
|
|
842
|
+
"""Apply updates to a memory.
|
|
843
|
+
|
|
844
|
+
Args:
|
|
845
|
+
memory: The memory to update.
|
|
846
|
+
|
|
847
|
+
Returns:
|
|
848
|
+
Updated memory.
|
|
849
|
+
"""
|
|
850
|
+
if self.content is not None:
|
|
851
|
+
memory.content = self.content
|
|
852
|
+
if self.category is not None:
|
|
853
|
+
memory.category = self.category
|
|
854
|
+
if self.confidence is not None:
|
|
855
|
+
memory.confidence = self.confidence
|
|
856
|
+
if self.importance is not None:
|
|
857
|
+
memory.importance = self.importance
|
|
858
|
+
if self.tags is not None:
|
|
859
|
+
memory.tags = self.tags
|
|
860
|
+
if self.entities is not None:
|
|
861
|
+
memory.entities = self.entities
|
|
862
|
+
if self.metadata is not None:
|
|
863
|
+
memory.metadata = self.metadata
|
|
864
|
+
memory.updated_at = utc_now()
|
|
865
|
+
return memory
|
|
866
|
+
|
|
867
|
+
|
|
868
|
+
class SearchQuery(BaseModel):
|
|
869
|
+
"""Pydantic model for search requests."""
|
|
870
|
+
|
|
871
|
+
model_config = ConfigDict(str_strip_whitespace=True)
|
|
872
|
+
|
|
873
|
+
query: str = Field(..., min_length=1, max_length=1000)
|
|
874
|
+
"""Search query text."""
|
|
875
|
+
|
|
876
|
+
project: str | None = Field(default=None, max_length=255)
|
|
877
|
+
"""Filter by project."""
|
|
878
|
+
|
|
879
|
+
categories: list[MemoryCategory] | None = None
|
|
880
|
+
"""Filter by categories."""
|
|
881
|
+
|
|
882
|
+
scope: MemoryScope | None = None
|
|
883
|
+
"""Filter by scope."""
|
|
884
|
+
|
|
885
|
+
include_archived: bool = False
|
|
886
|
+
"""Whether to include archived memories."""
|
|
887
|
+
|
|
888
|
+
min_score: float = Field(default=0.0, ge=-1.0, le=1.0)
|
|
889
|
+
"""Minimum outcome score filter."""
|
|
890
|
+
|
|
891
|
+
limit: int = Field(default=10, ge=1, le=100)
|
|
892
|
+
"""Maximum results to return."""
|
|
893
|
+
|
|
894
|
+
offset: int = Field(default=0, ge=0)
|
|
895
|
+
"""Results offset for pagination."""
|
|
896
|
+
|
|
897
|
+
|
|
898
|
+
class OutcomeRecord(BaseModel):
|
|
899
|
+
"""Pydantic model for recording an outcome."""
|
|
900
|
+
|
|
901
|
+
memory_ids: list[str] = Field(..., min_length=1, max_length=50)
|
|
902
|
+
"""IDs of memories to apply outcome to."""
|
|
903
|
+
|
|
904
|
+
outcome: Outcome
|
|
905
|
+
"""The outcome to record."""
|
|
906
|
+
|
|
907
|
+
|
|
908
|
+
class MemoryResponse(BaseModel):
|
|
909
|
+
"""Pydantic model for memory API responses."""
|
|
910
|
+
|
|
911
|
+
model_config = ConfigDict(from_attributes=True)
|
|
912
|
+
|
|
913
|
+
id: str
|
|
914
|
+
content: str
|
|
915
|
+
category: MemoryCategory
|
|
916
|
+
outcome_score: float
|
|
917
|
+
confidence: float
|
|
918
|
+
importance: float
|
|
919
|
+
use_count: int
|
|
920
|
+
project: str | None
|
|
921
|
+
scope: MemoryScope
|
|
922
|
+
source: MemorySource
|
|
923
|
+
tags: list[str]
|
|
924
|
+
entities: list[str]
|
|
925
|
+
supersedes: str | None
|
|
926
|
+
archived: bool
|
|
927
|
+
created_at: datetime
|
|
928
|
+
updated_at: datetime
|
|
929
|
+
metadata: dict[str, Any]
|
|
930
|
+
|
|
931
|
+
@classmethod
|
|
932
|
+
def from_memory(cls, memory: Memory) -> MemoryResponse:
|
|
933
|
+
"""Create response from Memory dataclass.
|
|
934
|
+
|
|
935
|
+
Args:
|
|
936
|
+
memory: The memory to convert.
|
|
937
|
+
|
|
938
|
+
Returns:
|
|
939
|
+
MemoryResponse instance.
|
|
940
|
+
"""
|
|
941
|
+
return cls(
|
|
942
|
+
id=memory.id,
|
|
943
|
+
content=memory.content,
|
|
944
|
+
category=memory.category,
|
|
945
|
+
outcome_score=memory.outcome_score,
|
|
946
|
+
confidence=memory.confidence,
|
|
947
|
+
importance=memory.importance,
|
|
948
|
+
use_count=memory.use_count,
|
|
949
|
+
project=memory.project,
|
|
950
|
+
scope=memory.scope,
|
|
951
|
+
source=memory.source,
|
|
952
|
+
tags=memory.tags,
|
|
953
|
+
entities=memory.entities,
|
|
954
|
+
supersedes=memory.supersedes,
|
|
955
|
+
archived=memory.archived,
|
|
956
|
+
created_at=memory.created_at,
|
|
957
|
+
updated_at=memory.updated_at,
|
|
958
|
+
metadata=memory.metadata,
|
|
959
|
+
)
|
|
960
|
+
|
|
961
|
+
|
|
962
|
+
class SearchResultResponse(BaseModel):
|
|
963
|
+
"""Pydantic model for search result API responses."""
|
|
964
|
+
|
|
965
|
+
model_config = ConfigDict(from_attributes=True)
|
|
966
|
+
|
|
967
|
+
memory: MemoryResponse
|
|
968
|
+
score: float
|
|
969
|
+
semantic_score: float
|
|
970
|
+
recency_score: float
|
|
971
|
+
frequency_score: float
|
|
972
|
+
category_boost: float
|
|
973
|
+
|
|
974
|
+
@classmethod
|
|
975
|
+
def from_search_result(cls, result: SearchResult) -> SearchResultResponse:
|
|
976
|
+
"""Create response from SearchResult dataclass.
|
|
977
|
+
|
|
978
|
+
Args:
|
|
979
|
+
result: The search result to convert.
|
|
980
|
+
|
|
981
|
+
Returns:
|
|
982
|
+
SearchResultResponse instance.
|
|
983
|
+
"""
|
|
984
|
+
return cls(
|
|
985
|
+
memory=MemoryResponse.from_memory(result.memory),
|
|
986
|
+
score=result.score,
|
|
987
|
+
semantic_score=result.semantic_score,
|
|
988
|
+
recency_score=result.recency_score,
|
|
989
|
+
frequency_score=result.frequency_score,
|
|
990
|
+
category_boost=result.category_boost,
|
|
991
|
+
)
|
|
992
|
+
|
|
993
|
+
|
|
994
|
+
class ContextResponseModel(BaseModel):
|
|
995
|
+
"""Pydantic model for context API responses."""
|
|
996
|
+
|
|
997
|
+
model_config = ConfigDict(from_attributes=True)
|
|
998
|
+
|
|
999
|
+
memories: list[MemoryResponse]
|
|
1000
|
+
project: str | None
|
|
1001
|
+
total_count: int
|
|
1002
|
+
included_count: int
|
|
1003
|
+
formatted: str
|
|
1004
|
+
categories: dict[str, int]
|
|
1005
|
+
|
|
1006
|
+
@classmethod
|
|
1007
|
+
def from_context_response(cls, response: ContextResponse) -> ContextResponseModel:
|
|
1008
|
+
"""Create response from ContextResponse dataclass.
|
|
1009
|
+
|
|
1010
|
+
Args:
|
|
1011
|
+
response: The context response to convert.
|
|
1012
|
+
|
|
1013
|
+
Returns:
|
|
1014
|
+
ContextResponseModel instance.
|
|
1015
|
+
"""
|
|
1016
|
+
return cls(
|
|
1017
|
+
memories=[MemoryResponse.from_memory(m) for m in response.memories],
|
|
1018
|
+
project=response.project,
|
|
1019
|
+
total_count=response.total_count,
|
|
1020
|
+
included_count=response.included_count,
|
|
1021
|
+
formatted=response.formatted or response.to_markdown(),
|
|
1022
|
+
categories=response.categories,
|
|
1023
|
+
)
|
|
1024
|
+
|
|
1025
|
+
|
|
1026
|
+
class StatsResponse(BaseModel):
|
|
1027
|
+
"""Pydantic model for statistics API responses."""
|
|
1028
|
+
|
|
1029
|
+
total_memories: int
|
|
1030
|
+
"""Total number of memories."""
|
|
1031
|
+
|
|
1032
|
+
active_memories: int
|
|
1033
|
+
"""Non-archived memories."""
|
|
1034
|
+
|
|
1035
|
+
archived_memories: int
|
|
1036
|
+
"""Archived memories."""
|
|
1037
|
+
|
|
1038
|
+
by_category: dict[str, int]
|
|
1039
|
+
"""Count by category."""
|
|
1040
|
+
|
|
1041
|
+
by_scope: dict[str, int]
|
|
1042
|
+
"""Count by scope."""
|
|
1043
|
+
|
|
1044
|
+
by_source: dict[str, int]
|
|
1045
|
+
"""Count by source."""
|
|
1046
|
+
|
|
1047
|
+
avg_outcome_score: float
|
|
1048
|
+
"""Average outcome score."""
|
|
1049
|
+
|
|
1050
|
+
total_uses: int
|
|
1051
|
+
"""Total use count across all memories."""
|