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.
Files changed (54) hide show
  1. runtime_memory/__init__.py +28 -0
  2. runtime_memory/claude_code/__init__.py +48 -0
  3. runtime_memory/claude_code/commands.py +698 -0
  4. runtime_memory/claude_code/daemon.py +852 -0
  5. runtime_memory/claude_code/hooks.py +722 -0
  6. runtime_memory/cli/__init__.py +8 -0
  7. runtime_memory/cli/main.py +1936 -0
  8. runtime_memory/core/__init__.py +216 -0
  9. runtime_memory/core/config.py +473 -0
  10. runtime_memory/core/embeddings.py +908 -0
  11. runtime_memory/core/engine.py +1007 -0
  12. runtime_memory/core/exceptions.py +547 -0
  13. runtime_memory/core/legacy_env.py +39 -0
  14. runtime_memory/core/logging.py +160 -0
  15. runtime_memory/core/models.py +1051 -0
  16. runtime_memory/core/observability.py +725 -0
  17. runtime_memory/core/paths.py +30 -0
  18. runtime_memory/core/resilience.py +511 -0
  19. runtime_memory/core/retrieval.py +819 -0
  20. runtime_memory/core/storage.py +1105 -0
  21. runtime_memory/extraction/__init__.py +36 -0
  22. runtime_memory/extraction/extractor.py +1143 -0
  23. runtime_memory/hermes/__init__.py +39 -0
  24. runtime_memory/hermes/_base.py +154 -0
  25. runtime_memory/hermes/bridge.py +119 -0
  26. runtime_memory/hermes/plugin.yaml +13 -0
  27. runtime_memory/hermes/provider.py +536 -0
  28. runtime_memory/hermes/tools.py +230 -0
  29. runtime_memory/hermes/trace.py +177 -0
  30. runtime_memory/plugin/__init__.py +646 -0
  31. runtime_memory/sdk/__init__.py +97 -0
  32. runtime_memory/sdk/client.py +1577 -0
  33. runtime_memory/server/__init__.py +75 -0
  34. runtime_memory/server/api.py +1665 -0
  35. runtime_memory/server/mcp.py +1574 -0
  36. runtime_memory/server/static/css/styles.css +1110 -0
  37. runtime_memory/server/static/index.html +264 -0
  38. runtime_memory/server/static/js/api.js +294 -0
  39. runtime_memory/server/static/js/app.js +771 -0
  40. runtime_memory/tasks/__init__.py +114 -0
  41. runtime_memory/tasks/adapter.py +501 -0
  42. runtime_memory/tasks/claude_code_adapter.py +495 -0
  43. runtime_memory/tasks/claude_code_parser.py +339 -0
  44. runtime_memory/tasks/cli_bridge.py +415 -0
  45. runtime_memory/tasks/linking.py +397 -0
  46. runtime_memory/tasks/models.py +520 -0
  47. runtime_memory/tasks/outcomes.py +320 -0
  48. runtime_memory/tasks/parser.py +305 -0
  49. runtime_memory/tasks/unified_adapter.py +661 -0
  50. runtime_memory-3.0.0.dist-info/METADATA +497 -0
  51. runtime_memory-3.0.0.dist-info/RECORD +54 -0
  52. runtime_memory-3.0.0.dist-info/WHEEL +4 -0
  53. runtime_memory-3.0.0.dist-info/entry_points.txt +6 -0
  54. 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."""