rootmemory 0.1.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.
- rootmemory/__init__.py +84 -0
- rootmemory/api/__init__.py +25 -0
- rootmemory/api/agents.py +38 -0
- rootmemory/api/beliefs.py +66 -0
- rootmemory/api/claims.py +87 -0
- rootmemory/api/deps.py +21 -0
- rootmemory/api/errors.py +36 -0
- rootmemory/api/graph.py +32 -0
- rootmemory/api/invalidation.py +47 -0
- rootmemory/api/observations.py +46 -0
- rootmemory/api/promotion.py +37 -0
- rootmemory/api/provenance.py +88 -0
- rootmemory/api/security.py +53 -0
- rootmemory/cli.py +96 -0
- rootmemory/client.py +219 -0
- rootmemory/config.py +64 -0
- rootmemory/db/__init__.py +0 -0
- rootmemory/db/base.py +42 -0
- rootmemory/db/migrations/env.py +59 -0
- rootmemory/db/migrations/script.py.mako +26 -0
- rootmemory/db/migrations/versions/d336d5cf7fb9_initial_schema.py +260 -0
- rootmemory/db/session.py +138 -0
- rootmemory/errors.py +27 -0
- rootmemory/integrations/__init__.py +16 -0
- rootmemory/integrations/async_client.py +268 -0
- rootmemory/integrations/langchain_tools.py +237 -0
- rootmemory/integrations/langgraph_memory.py +179 -0
- rootmemory/integrations/langgraph_store.py +402 -0
- rootmemory/logging_config.py +30 -0
- rootmemory/main.py +133 -0
- rootmemory/models/__init__.py +22 -0
- rootmemory/models/agent.py +30 -0
- rootmemory/models/audit.py +51 -0
- rootmemory/models/belief.py +39 -0
- rootmemory/models/claim.py +52 -0
- rootmemory/models/decision.py +38 -0
- rootmemory/models/edge.py +43 -0
- rootmemory/models/enums.py +104 -0
- rootmemory/models/node_ref.py +42 -0
- rootmemory/models/observation.py +51 -0
- rootmemory/py.typed +0 -0
- rootmemory/repositories/__init__.py +19 -0
- rootmemory/repositories/agent_repo.py +44 -0
- rootmemory/repositories/audit_repo.py +43 -0
- rootmemory/repositories/belief_repo.py +63 -0
- rootmemory/repositories/claim_repo.py +102 -0
- rootmemory/repositories/decision_repo.py +76 -0
- rootmemory/repositories/edge_repo.py +77 -0
- rootmemory/repositories/observation_repo.py +76 -0
- rootmemory/schemas/__init__.py +52 -0
- rootmemory/schemas/agent.py +26 -0
- rootmemory/schemas/belief.py +49 -0
- rootmemory/schemas/claim.py +54 -0
- rootmemory/schemas/common.py +27 -0
- rootmemory/schemas/observation.py +69 -0
- rootmemory/schemas/promotion.py +50 -0
- rootmemory/schemas/provenance.py +51 -0
- rootmemory/services/__init__.py +35 -0
- rootmemory/services/belief_service.py +161 -0
- rootmemory/services/contradiction_service.py +148 -0
- rootmemory/services/decision_service.py +65 -0
- rootmemory/services/graph_service.py +316 -0
- rootmemory/services/independence_service.py +159 -0
- rootmemory/services/invalidation_service.py +164 -0
- rootmemory/services/normalization_service.py +234 -0
- rootmemory/services/promotion_service.py +318 -0
- rootmemory/services/provenance_service.py +282 -0
- rootmemory/services/scoring_service.py +53 -0
- rootmemory/static/index.html +866 -0
- rootmemory-0.1.0.dist-info/METADATA +266 -0
- rootmemory-0.1.0.dist-info/RECORD +75 -0
- rootmemory-0.1.0.dist-info/WHEEL +5 -0
- rootmemory-0.1.0.dist-info/entry_points.txt +2 -0
- rootmemory-0.1.0.dist-info/licenses/LICENSE +21 -0
- rootmemory-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Beliefs: agent interpretations of evidence (spec section 11)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
from sqlalchemy import Float, ForeignKey, String, Text
|
|
9
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
10
|
+
|
|
11
|
+
from rootmemory.db.base import Base, json_column, pk_column, timestamp_column, utcnow
|
|
12
|
+
from rootmemory.models.enums import EpistemicStatus, Visibility
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Belief(Base):
|
|
16
|
+
"""An interpretation produced by one agent. A belief is not a fact.
|
|
17
|
+
|
|
18
|
+
Every belief must have at least one causal parent (an observation or
|
|
19
|
+
another belief). A belief with no provenance can never enter shared memory
|
|
20
|
+
(spec section 16).
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
__tablename__ = "beliefs"
|
|
24
|
+
|
|
25
|
+
id: Mapped[uuid.UUID] = pk_column()
|
|
26
|
+
agent_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("agents.id"), index=True)
|
|
27
|
+
claim_text: Mapped[str] = mapped_column(Text)
|
|
28
|
+
normalized_claim_key: Mapped[str] = mapped_column(String(300), index=True)
|
|
29
|
+
confidence: Mapped[float] = mapped_column(Float, default=0.5)
|
|
30
|
+
epistemic_status: Mapped[str] = mapped_column(
|
|
31
|
+
String(20), default=EpistemicStatus.INFERRED, index=True
|
|
32
|
+
)
|
|
33
|
+
visibility: Mapped[str] = mapped_column(String(20), default=Visibility.PRIVATE, index=True)
|
|
34
|
+
# True when the belief text was produced by an LLM rather than by
|
|
35
|
+
# deterministic code (spec section 47).
|
|
36
|
+
llm_generated: Mapped[bool] = mapped_column(default=False)
|
|
37
|
+
created_at: Mapped[datetime] = timestamp_column()
|
|
38
|
+
updated_at: Mapped[datetime] = timestamp_column(onupdate=utcnow)
|
|
39
|
+
meta: Mapped[dict] = json_column()
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Claims: normalized propositions that beliefs support or contradict."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
from sqlalchemy import DateTime, Float, ForeignKey, Integer, String, Text, UniqueConstraint
|
|
9
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
10
|
+
|
|
11
|
+
from rootmemory.db.base import Base, json_column, pk_column, timestamp_column
|
|
12
|
+
from rootmemory.models.enums import ClaimStatus, LinkRole
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Claim(Base):
|
|
16
|
+
"""A proposition that several beliefs may map onto.
|
|
17
|
+
|
|
18
|
+
Only the PromotionService may set status to confirmed (spec section 46).
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
__tablename__ = "claims"
|
|
22
|
+
|
|
23
|
+
id: Mapped[uuid.UUID] = pk_column()
|
|
24
|
+
claim_key: Mapped[str] = mapped_column(String(300), unique=True, index=True)
|
|
25
|
+
canonical_text: Mapped[str] = mapped_column(Text)
|
|
26
|
+
status: Mapped[str] = mapped_column(String(20), default=ClaimStatus.UNCONFIRMED, index=True)
|
|
27
|
+
confidence: Mapped[float] = mapped_column(Float, default=0.0)
|
|
28
|
+
independent_support_count: Mapped[int] = mapped_column(Integer, default=0)
|
|
29
|
+
independent_contradiction_count: Mapped[int] = mapped_column(Integer, default=0)
|
|
30
|
+
# Timezone-aware like every other timestamp: PromotionService writes an
|
|
31
|
+
# aware datetime, and a naive column would silently strip the offset on
|
|
32
|
+
# PostgreSQL (SQLite does not notice, which is how this hid).
|
|
33
|
+
promoted_at: Mapped[datetime | None] = mapped_column(
|
|
34
|
+
DateTime(timezone=True), default=None
|
|
35
|
+
)
|
|
36
|
+
created_at: Mapped[datetime] = timestamp_column()
|
|
37
|
+
meta: Mapped[dict] = json_column()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ClaimBeliefLink(Base):
|
|
41
|
+
"""Association between a claim and a belief that supports or contradicts it."""
|
|
42
|
+
|
|
43
|
+
__tablename__ = "claim_belief_links"
|
|
44
|
+
__table_args__ = (
|
|
45
|
+
UniqueConstraint("claim_id", "belief_id", "role", name="claim_id_belief_id_role"),
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
id: Mapped[uuid.UUID] = pk_column()
|
|
49
|
+
claim_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("claims.id"), index=True)
|
|
50
|
+
belief_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("beliefs.id"), index=True)
|
|
51
|
+
role: Mapped[str] = mapped_column(String(20), default=LinkRole.SUPPORTS, index=True)
|
|
52
|
+
created_at: Mapped[datetime] = timestamp_column()
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Decisions: downstream outputs that depend on claims (spec section 14)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
from sqlalchemy import ForeignKey, String, Text, UniqueConstraint
|
|
9
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
10
|
+
|
|
11
|
+
from rootmemory.db.base import Base, json_column, pk_column, timestamp_column
|
|
12
|
+
from rootmemory.models.enums import DecisionStatus
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Decision(Base):
|
|
16
|
+
"""An action or conclusion an agent produced from one or more claims."""
|
|
17
|
+
|
|
18
|
+
__tablename__ = "decisions"
|
|
19
|
+
|
|
20
|
+
id: Mapped[uuid.UUID] = pk_column()
|
|
21
|
+
agent_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("agents.id"), index=True)
|
|
22
|
+
decision_type: Mapped[str] = mapped_column(String(100), index=True)
|
|
23
|
+
content: Mapped[str] = mapped_column(Text)
|
|
24
|
+
status: Mapped[str] = mapped_column(String(20), default=DecisionStatus.ACTIVE, index=True)
|
|
25
|
+
created_at: Mapped[datetime] = timestamp_column()
|
|
26
|
+
meta: Mapped[dict] = json_column()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class DecisionDependency(Base):
|
|
30
|
+
"""Explicit link from a decision to a claim it relies on."""
|
|
31
|
+
|
|
32
|
+
__tablename__ = "decision_dependencies"
|
|
33
|
+
__table_args__ = (UniqueConstraint("decision_id", "claim_id", name="decision_id_claim_id"),)
|
|
34
|
+
|
|
35
|
+
id: Mapped[uuid.UUID] = pk_column()
|
|
36
|
+
decision_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("decisions.id"), index=True)
|
|
37
|
+
claim_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("claims.id"), index=True)
|
|
38
|
+
created_at: Mapped[datetime] = timestamp_column()
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Provenance edges: the causal dependency graph (spec section 12)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
from sqlalchemy import Float, Index, String, UniqueConstraint, Uuid
|
|
9
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
10
|
+
|
|
11
|
+
from rootmemory.db.base import Base, json_column, pk_column, timestamp_column
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ProvenanceEdge(Base):
|
|
15
|
+
"""A directed edge between two knowledge nodes.
|
|
16
|
+
|
|
17
|
+
Edges are polymorphic (they connect observations, beliefs, claims and
|
|
18
|
+
decisions) so the endpoints are stored as (id, type) pairs rather than as
|
|
19
|
+
foreign keys into a single table.
|
|
20
|
+
|
|
21
|
+
Edges are append-only in normal operation: nothing in the service layer
|
|
22
|
+
updates or deletes them.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
__tablename__ = "provenance_edges"
|
|
26
|
+
__table_args__ = (
|
|
27
|
+
UniqueConstraint(
|
|
28
|
+
"from_node_id", "to_node_id", "edge_type", name="from_node_id_to_node_id_edge_type"
|
|
29
|
+
),
|
|
30
|
+
Index("ix_provenance_edges_from_node_id", "from_node_id"),
|
|
31
|
+
Index("ix_provenance_edges_to_node_id", "to_node_id"),
|
|
32
|
+
Index("ix_provenance_edges_edge_type", "edge_type"),
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
id: Mapped[uuid.UUID] = pk_column()
|
|
36
|
+
from_node_id: Mapped[uuid.UUID] = mapped_column(Uuid(as_uuid=True))
|
|
37
|
+
from_node_type: Mapped[str] = mapped_column(String(20))
|
|
38
|
+
to_node_id: Mapped[uuid.UUID] = mapped_column(Uuid(as_uuid=True))
|
|
39
|
+
to_node_type: Mapped[str] = mapped_column(String(20))
|
|
40
|
+
edge_type: Mapped[str] = mapped_column(String(20))
|
|
41
|
+
weight: Mapped[float] = mapped_column(Float, default=1.0)
|
|
42
|
+
created_at: Mapped[datetime] = timestamp_column()
|
|
43
|
+
meta: Mapped[dict] = json_column()
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Enumerations shared by the ORM models, schemas and services.
|
|
2
|
+
|
|
3
|
+
All of these are stored as plain strings so the schema is portable across
|
|
4
|
+
SQLite and PostgreSQL and so new values never require a migration.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from enum import StrEnum
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class NodeType(StrEnum):
|
|
13
|
+
"""The four knowledge objects that can appear in the provenance graph."""
|
|
14
|
+
|
|
15
|
+
OBSERVATION = "observation"
|
|
16
|
+
BELIEF = "belief"
|
|
17
|
+
CLAIM = "claim"
|
|
18
|
+
DECISION = "decision"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class EdgeType(StrEnum):
|
|
22
|
+
"""Provenance edge types.
|
|
23
|
+
|
|
24
|
+
Direction convention, applied consistently across the whole system:
|
|
25
|
+
|
|
26
|
+
an edge always points from the *dependent* node to the node it
|
|
27
|
+
*depends on* (child -> parent).
|
|
28
|
+
|
|
29
|
+
belief --derived_from--> observation | belief
|
|
30
|
+
decision --depends_on----> claim
|
|
31
|
+
belief --supports-----> claim (annotation, see below)
|
|
32
|
+
belief --contradicts--> claim (annotation, see below)
|
|
33
|
+
|
|
34
|
+
``derived_from`` and ``depends_on`` are the *causal* edges: they are the
|
|
35
|
+
ones walked by evidence-root discovery and by cycle detection.
|
|
36
|
+
|
|
37
|
+
``supports`` / ``contradicts`` are recorded for audit and read naturally
|
|
38
|
+
left-to-right ("belief supports claim"). Because a claim is downstream of
|
|
39
|
+
the beliefs that support it, they are NOT walked as causal parent edges;
|
|
40
|
+
claim membership is resolved through ``claim_belief_links``.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
DERIVED_FROM = "derived_from"
|
|
44
|
+
SUPPORTS = "supports"
|
|
45
|
+
CONTRADICTS = "contradicts"
|
|
46
|
+
DEPENDS_ON = "depends_on"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
#: Edge types that express causal ancestry (dependent -> dependency).
|
|
50
|
+
CAUSAL_EDGE_TYPES: frozenset[str] = frozenset({EdgeType.DERIVED_FROM, EdgeType.DEPENDS_ON})
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class ValidityStatus(StrEnum):
|
|
54
|
+
"""Validity of an observation. Observations are immutable; only this moves."""
|
|
55
|
+
|
|
56
|
+
VALID = "valid"
|
|
57
|
+
INVALID = "invalid"
|
|
58
|
+
DISPUTED = "disputed"
|
|
59
|
+
UNKNOWN = "unknown"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class EpistemicStatus(StrEnum):
|
|
63
|
+
"""How strongly an agent currently holds a belief."""
|
|
64
|
+
|
|
65
|
+
INFERRED = "inferred"
|
|
66
|
+
BELIEVED = "believed"
|
|
67
|
+
UNCERTAIN = "uncertain"
|
|
68
|
+
CONFIRMED = "confirmed"
|
|
69
|
+
CONTRADICTED = "contradicted"
|
|
70
|
+
RETRACTED = "retracted"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class Visibility(StrEnum):
|
|
74
|
+
"""Private reasoning vs shared memory (spec section 29)."""
|
|
75
|
+
|
|
76
|
+
PRIVATE = "private"
|
|
77
|
+
CANDIDATE_SHARED = "candidate_shared"
|
|
78
|
+
SHARED = "shared"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class ClaimStatus(StrEnum):
|
|
82
|
+
UNCONFIRMED = "unconfirmed"
|
|
83
|
+
PROVISIONAL = "provisional"
|
|
84
|
+
CONFIRMED = "confirmed"
|
|
85
|
+
CONTRADICTED = "contradicted"
|
|
86
|
+
RETRACTED = "retracted"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class DecisionStatus(StrEnum):
|
|
90
|
+
ACTIVE = "active"
|
|
91
|
+
NEEDS_REVIEW = "needs_review"
|
|
92
|
+
INVALIDATED = "invalidated"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class LinkRole(StrEnum):
|
|
96
|
+
"""How a belief relates to a claim."""
|
|
97
|
+
|
|
98
|
+
SUPPORTS = "supports"
|
|
99
|
+
CONTRADICTS = "contradicts"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class PromotionResultType(StrEnum):
|
|
103
|
+
PROMOTED = "promoted"
|
|
104
|
+
REJECTED = "rejected"
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""NodeRef: a typed pointer to any node in the provenance graph."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
from rootmemory.models.enums import NodeType
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True, slots=True)
|
|
12
|
+
class NodeRef:
|
|
13
|
+
"""A (id, type) pair identifying one node.
|
|
14
|
+
|
|
15
|
+
The provenance graph spans four tables, so edges and traversal results
|
|
16
|
+
address nodes by this pair rather than by a bare id.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
id: uuid.UUID
|
|
20
|
+
type: NodeType
|
|
21
|
+
|
|
22
|
+
def __str__(self) -> str:
|
|
23
|
+
return f"{self.type}:{self.id}"
|
|
24
|
+
|
|
25
|
+
def as_dict(self) -> dict[str, str]:
|
|
26
|
+
return {"id": str(self.id), "type": str(self.type)}
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def observation(cls, node_id: uuid.UUID) -> NodeRef:
|
|
30
|
+
return cls(node_id, NodeType.OBSERVATION)
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def belief(cls, node_id: uuid.UUID) -> NodeRef:
|
|
34
|
+
return cls(node_id, NodeType.BELIEF)
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def claim(cls, node_id: uuid.UUID) -> NodeRef:
|
|
38
|
+
return cls(node_id, NodeType.CLAIM)
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def decision(cls, node_id: uuid.UUID) -> NodeRef:
|
|
42
|
+
return cls(node_id, NodeType.DECISION)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Observations: immutable external evidence (spec section 10)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
from sqlalchemy import Float, ForeignKey, String, Text
|
|
9
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
10
|
+
|
|
11
|
+
from rootmemory.db.base import Base, json_column, pk_column, timestamp_column
|
|
12
|
+
from rootmemory.models.enums import ValidityStatus
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Observation(Base):
|
|
16
|
+
"""A raw piece of evidence from outside the system.
|
|
17
|
+
|
|
18
|
+
Observations are append-only. When one turns out to be wrong its
|
|
19
|
+
``validity_status`` changes; the row itself is never edited or deleted, so
|
|
20
|
+
history stays reconstructable.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
__tablename__ = "observations"
|
|
24
|
+
|
|
25
|
+
id: Mapped[uuid.UUID] = pk_column()
|
|
26
|
+
source_type: Mapped[str] = mapped_column(String(100), index=True)
|
|
27
|
+
source_uri: Mapped[str | None] = mapped_column(String(1000), default=None)
|
|
28
|
+
source_actor: Mapped[str | None] = mapped_column(String(300), default=None)
|
|
29
|
+
content: Mapped[str] = mapped_column(Text)
|
|
30
|
+
content_hash: Mapped[str] = mapped_column(String(64), index=True)
|
|
31
|
+
timestamp: Mapped[datetime] = timestamp_column()
|
|
32
|
+
created_by_agent_id: Mapped[uuid.UUID | None] = mapped_column(
|
|
33
|
+
ForeignKey("agents.id"), default=None, index=True
|
|
34
|
+
)
|
|
35
|
+
validity_status: Mapped[str] = mapped_column(
|
|
36
|
+
String(20), default=ValidityStatus.VALID, index=True
|
|
37
|
+
)
|
|
38
|
+
validity_reason: Mapped[str | None] = mapped_column(String(2000), default=None)
|
|
39
|
+
reliability_score: Mapped[float] = mapped_column(Float, default=1.0)
|
|
40
|
+
# Two observations that copy the same original source share a family id and
|
|
41
|
+
# therefore count as ONE independent source (spec section 20).
|
|
42
|
+
source_family_id: Mapped[str | None] = mapped_column(String(200), default=None, index=True)
|
|
43
|
+
created_at: Mapped[datetime] = timestamp_column()
|
|
44
|
+
meta: Mapped[dict] = json_column()
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def independence_key(self) -> str:
|
|
48
|
+
"""The key this observation contributes to an independence count."""
|
|
49
|
+
if self.source_family_id:
|
|
50
|
+
return f"family:{self.source_family_id}"
|
|
51
|
+
return f"obs:{self.id}"
|
rootmemory/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Repository layer: all SQL lives here, no business rules."""
|
|
2
|
+
|
|
3
|
+
from rootmemory.repositories.agent_repo import AgentRepository
|
|
4
|
+
from rootmemory.repositories.audit_repo import AuditRepository
|
|
5
|
+
from rootmemory.repositories.belief_repo import BeliefRepository
|
|
6
|
+
from rootmemory.repositories.claim_repo import ClaimRepository
|
|
7
|
+
from rootmemory.repositories.decision_repo import DecisionRepository
|
|
8
|
+
from rootmemory.repositories.edge_repo import EdgeRepository
|
|
9
|
+
from rootmemory.repositories.observation_repo import ObservationRepository
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"AgentRepository",
|
|
13
|
+
"AuditRepository",
|
|
14
|
+
"BeliefRepository",
|
|
15
|
+
"ClaimRepository",
|
|
16
|
+
"DecisionRepository",
|
|
17
|
+
"EdgeRepository",
|
|
18
|
+
"ObservationRepository",
|
|
19
|
+
]
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Data access for agents."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
from collections.abc import Sequence
|
|
7
|
+
|
|
8
|
+
from sqlalchemy import select
|
|
9
|
+
from sqlalchemy.orm import Session
|
|
10
|
+
|
|
11
|
+
from rootmemory.models.agent import Agent
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AgentRepository:
|
|
15
|
+
def __init__(self, session: Session) -> None:
|
|
16
|
+
self.session = session
|
|
17
|
+
|
|
18
|
+
def create(
|
|
19
|
+
self,
|
|
20
|
+
name: str,
|
|
21
|
+
role: str = "",
|
|
22
|
+
description: str = "",
|
|
23
|
+
trust_score: float = 0.5,
|
|
24
|
+
meta: dict | None = None,
|
|
25
|
+
) -> Agent:
|
|
26
|
+
agent = Agent(
|
|
27
|
+
name=name,
|
|
28
|
+
role=role,
|
|
29
|
+
description=description,
|
|
30
|
+
trust_score=trust_score,
|
|
31
|
+
meta=meta or {},
|
|
32
|
+
)
|
|
33
|
+
self.session.add(agent)
|
|
34
|
+
self.session.flush()
|
|
35
|
+
return agent
|
|
36
|
+
|
|
37
|
+
def get(self, agent_id: uuid.UUID) -> Agent | None:
|
|
38
|
+
return self.session.get(Agent, agent_id)
|
|
39
|
+
|
|
40
|
+
def get_by_name(self, name: str) -> Agent | None:
|
|
41
|
+
return self.session.execute(select(Agent).where(Agent.name == name)).scalar_one_or_none()
|
|
42
|
+
|
|
43
|
+
def list(self) -> Sequence[Agent]:
|
|
44
|
+
return self.session.execute(select(Agent).order_by(Agent.created_at)).scalars().all()
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Data access for promotion audits and repair reports."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
from collections.abc import Sequence
|
|
7
|
+
|
|
8
|
+
from sqlalchemy import select
|
|
9
|
+
from sqlalchemy.orm import Session
|
|
10
|
+
|
|
11
|
+
from rootmemory.models.audit import PromotionAudit, RepairReport
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AuditRepository:
|
|
15
|
+
def __init__(self, session: Session) -> None:
|
|
16
|
+
self.session = session
|
|
17
|
+
|
|
18
|
+
def record_promotion(self, **fields: object) -> PromotionAudit:
|
|
19
|
+
audit = PromotionAudit(**fields)
|
|
20
|
+
self.session.add(audit)
|
|
21
|
+
self.session.flush()
|
|
22
|
+
return audit
|
|
23
|
+
|
|
24
|
+
def promotions_for_claim(self, claim_id: uuid.UUID) -> Sequence[PromotionAudit]:
|
|
25
|
+
stmt = (
|
|
26
|
+
select(PromotionAudit)
|
|
27
|
+
.where(PromotionAudit.claim_id == claim_id)
|
|
28
|
+
.order_by(PromotionAudit.evaluated_at)
|
|
29
|
+
)
|
|
30
|
+
return self.session.execute(stmt).scalars().all()
|
|
31
|
+
|
|
32
|
+
def record_repair(self, **fields: object) -> RepairReport:
|
|
33
|
+
report = RepairReport(**fields)
|
|
34
|
+
self.session.add(report)
|
|
35
|
+
self.session.flush()
|
|
36
|
+
return report
|
|
37
|
+
|
|
38
|
+
def get_repair(self, report_id: uuid.UUID) -> RepairReport | None:
|
|
39
|
+
return self.session.get(RepairReport, report_id)
|
|
40
|
+
|
|
41
|
+
def list_repairs(self) -> Sequence[RepairReport]:
|
|
42
|
+
stmt = select(RepairReport).order_by(RepairReport.created_at)
|
|
43
|
+
return self.session.execute(stmt).scalars().all()
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Data access for beliefs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
from collections.abc import Iterable, Sequence
|
|
7
|
+
|
|
8
|
+
from sqlalchemy import select
|
|
9
|
+
from sqlalchemy.orm import Session
|
|
10
|
+
|
|
11
|
+
from rootmemory.models.belief import Belief
|
|
12
|
+
from rootmemory.models.enums import EpistemicStatus, Visibility
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BeliefRepository:
|
|
16
|
+
def __init__(self, session: Session) -> None:
|
|
17
|
+
self.session = session
|
|
18
|
+
|
|
19
|
+
def create(
|
|
20
|
+
self,
|
|
21
|
+
agent_id: uuid.UUID,
|
|
22
|
+
claim_text: str,
|
|
23
|
+
normalized_claim_key: str,
|
|
24
|
+
confidence: float,
|
|
25
|
+
epistemic_status: EpistemicStatus = EpistemicStatus.INFERRED,
|
|
26
|
+
visibility: Visibility = Visibility.PRIVATE,
|
|
27
|
+
llm_generated: bool = False,
|
|
28
|
+
meta: dict | None = None,
|
|
29
|
+
) -> Belief:
|
|
30
|
+
belief = Belief(
|
|
31
|
+
agent_id=agent_id,
|
|
32
|
+
claim_text=claim_text,
|
|
33
|
+
normalized_claim_key=normalized_claim_key,
|
|
34
|
+
confidence=confidence,
|
|
35
|
+
epistemic_status=str(epistemic_status),
|
|
36
|
+
visibility=str(visibility),
|
|
37
|
+
llm_generated=llm_generated,
|
|
38
|
+
meta=meta or {},
|
|
39
|
+
)
|
|
40
|
+
self.session.add(belief)
|
|
41
|
+
self.session.flush()
|
|
42
|
+
return belief
|
|
43
|
+
|
|
44
|
+
def get(self, belief_id: uuid.UUID) -> Belief | None:
|
|
45
|
+
return self.session.get(Belief, belief_id)
|
|
46
|
+
|
|
47
|
+
def get_many(self, belief_ids: Iterable[uuid.UUID]) -> Sequence[Belief]:
|
|
48
|
+
ids = list(belief_ids)
|
|
49
|
+
if not ids:
|
|
50
|
+
return []
|
|
51
|
+
stmt = select(Belief).where(Belief.id.in_(ids))
|
|
52
|
+
return self.session.execute(stmt).scalars().all()
|
|
53
|
+
|
|
54
|
+
def list_by_claim_key(self, claim_key: str) -> Sequence[Belief]:
|
|
55
|
+
stmt = select(Belief).where(Belief.normalized_claim_key == claim_key)
|
|
56
|
+
return self.session.execute(stmt).scalars().all()
|
|
57
|
+
|
|
58
|
+
def list_by_agent(self, agent_id: uuid.UUID) -> Sequence[Belief]:
|
|
59
|
+
stmt = select(Belief).where(Belief.agent_id == agent_id).order_by(Belief.created_at)
|
|
60
|
+
return self.session.execute(stmt).scalars().all()
|
|
61
|
+
|
|
62
|
+
def list(self) -> Sequence[Belief]:
|
|
63
|
+
return self.session.execute(select(Belief).order_by(Belief.created_at)).scalars().all()
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Data access for claims and claim/belief links."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
from collections.abc import Sequence
|
|
7
|
+
|
|
8
|
+
from sqlalchemy import select
|
|
9
|
+
from sqlalchemy.orm import Session
|
|
10
|
+
|
|
11
|
+
from rootmemory.models.belief import Belief
|
|
12
|
+
from rootmemory.models.claim import Claim, ClaimBeliefLink
|
|
13
|
+
from rootmemory.models.enums import ClaimStatus, LinkRole
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ClaimRepository:
|
|
17
|
+
def __init__(self, session: Session) -> None:
|
|
18
|
+
self.session = session
|
|
19
|
+
|
|
20
|
+
def create(
|
|
21
|
+
self,
|
|
22
|
+
claim_key: str,
|
|
23
|
+
canonical_text: str,
|
|
24
|
+
meta: dict | None = None,
|
|
25
|
+
) -> Claim:
|
|
26
|
+
claim = Claim(claim_key=claim_key, canonical_text=canonical_text, meta=meta or {})
|
|
27
|
+
self.session.add(claim)
|
|
28
|
+
self.session.flush()
|
|
29
|
+
return claim
|
|
30
|
+
|
|
31
|
+
def get(self, claim_id: uuid.UUID) -> Claim | None:
|
|
32
|
+
return self.session.get(Claim, claim_id)
|
|
33
|
+
|
|
34
|
+
def get_for_update(self, claim_id: uuid.UUID) -> Claim | None:
|
|
35
|
+
"""Read a claim and hold a row lock until the transaction ends.
|
|
36
|
+
|
|
37
|
+
The promotion gate reads a claim's support, decides, then writes the
|
|
38
|
+
result. Without this lock two workers can interleave those steps and
|
|
39
|
+
confirm a claim on evidence the other one is retracting.
|
|
40
|
+
|
|
41
|
+
SQLAlchemy emits FOR UPDATE on PostgreSQL and omits it on SQLite, which
|
|
42
|
+
has no row locks and serializes writers anyway - so this is correct on
|
|
43
|
+
both without a dialect branch.
|
|
44
|
+
"""
|
|
45
|
+
stmt = select(Claim).where(Claim.id == claim_id).with_for_update()
|
|
46
|
+
return self.session.execute(stmt).scalar_one_or_none()
|
|
47
|
+
|
|
48
|
+
def get_by_key(self, claim_key: str) -> Claim | None:
|
|
49
|
+
stmt = select(Claim).where(Claim.claim_key == claim_key)
|
|
50
|
+
return self.session.execute(stmt).scalar_one_or_none()
|
|
51
|
+
|
|
52
|
+
def get_or_create(self, claim_key: str, canonical_text: str) -> Claim:
|
|
53
|
+
existing = self.get_by_key(claim_key)
|
|
54
|
+
if existing is not None:
|
|
55
|
+
return existing
|
|
56
|
+
return self.create(claim_key=claim_key, canonical_text=canonical_text)
|
|
57
|
+
|
|
58
|
+
def list(self) -> Sequence[Claim]:
|
|
59
|
+
return self.session.execute(select(Claim).order_by(Claim.created_at)).scalars().all()
|
|
60
|
+
|
|
61
|
+
def link_belief(
|
|
62
|
+
self, claim_id: uuid.UUID, belief_id: uuid.UUID, role: LinkRole
|
|
63
|
+
) -> ClaimBeliefLink:
|
|
64
|
+
existing = self.session.execute(
|
|
65
|
+
select(ClaimBeliefLink).where(
|
|
66
|
+
ClaimBeliefLink.claim_id == claim_id,
|
|
67
|
+
ClaimBeliefLink.belief_id == belief_id,
|
|
68
|
+
ClaimBeliefLink.role == str(role),
|
|
69
|
+
)
|
|
70
|
+
).scalar_one_or_none()
|
|
71
|
+
if existing is not None:
|
|
72
|
+
return existing
|
|
73
|
+
link = ClaimBeliefLink(claim_id=claim_id, belief_id=belief_id, role=str(role))
|
|
74
|
+
self.session.add(link)
|
|
75
|
+
self.session.flush()
|
|
76
|
+
return link
|
|
77
|
+
|
|
78
|
+
def beliefs_for_claim(self, claim_id: uuid.UUID, role: LinkRole) -> Sequence[Belief]:
|
|
79
|
+
stmt = (
|
|
80
|
+
select(Belief)
|
|
81
|
+
.join(ClaimBeliefLink, ClaimBeliefLink.belief_id == Belief.id)
|
|
82
|
+
.where(ClaimBeliefLink.claim_id == claim_id, ClaimBeliefLink.role == str(role))
|
|
83
|
+
.order_by(Belief.created_at)
|
|
84
|
+
)
|
|
85
|
+
return self.session.execute(stmt).scalars().all()
|
|
86
|
+
|
|
87
|
+
def claims_for_beliefs(self, belief_ids: Sequence[uuid.UUID]) -> Sequence[Claim]:
|
|
88
|
+
"""Every claim any of these beliefs supports or contradicts."""
|
|
89
|
+
if not belief_ids:
|
|
90
|
+
return []
|
|
91
|
+
stmt = (
|
|
92
|
+
select(Claim)
|
|
93
|
+
.join(ClaimBeliefLink, ClaimBeliefLink.claim_id == Claim.id)
|
|
94
|
+
.where(ClaimBeliefLink.belief_id.in_(list(belief_ids)))
|
|
95
|
+
.distinct()
|
|
96
|
+
)
|
|
97
|
+
return self.session.execute(stmt).scalars().all()
|
|
98
|
+
|
|
99
|
+
def set_status(self, claim: Claim, status: ClaimStatus) -> Claim:
|
|
100
|
+
claim.status = str(status)
|
|
101
|
+
self.session.flush()
|
|
102
|
+
return claim
|