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.
Files changed (75) hide show
  1. rootmemory/__init__.py +84 -0
  2. rootmemory/api/__init__.py +25 -0
  3. rootmemory/api/agents.py +38 -0
  4. rootmemory/api/beliefs.py +66 -0
  5. rootmemory/api/claims.py +87 -0
  6. rootmemory/api/deps.py +21 -0
  7. rootmemory/api/errors.py +36 -0
  8. rootmemory/api/graph.py +32 -0
  9. rootmemory/api/invalidation.py +47 -0
  10. rootmemory/api/observations.py +46 -0
  11. rootmemory/api/promotion.py +37 -0
  12. rootmemory/api/provenance.py +88 -0
  13. rootmemory/api/security.py +53 -0
  14. rootmemory/cli.py +96 -0
  15. rootmemory/client.py +219 -0
  16. rootmemory/config.py +64 -0
  17. rootmemory/db/__init__.py +0 -0
  18. rootmemory/db/base.py +42 -0
  19. rootmemory/db/migrations/env.py +59 -0
  20. rootmemory/db/migrations/script.py.mako +26 -0
  21. rootmemory/db/migrations/versions/d336d5cf7fb9_initial_schema.py +260 -0
  22. rootmemory/db/session.py +138 -0
  23. rootmemory/errors.py +27 -0
  24. rootmemory/integrations/__init__.py +16 -0
  25. rootmemory/integrations/async_client.py +268 -0
  26. rootmemory/integrations/langchain_tools.py +237 -0
  27. rootmemory/integrations/langgraph_memory.py +179 -0
  28. rootmemory/integrations/langgraph_store.py +402 -0
  29. rootmemory/logging_config.py +30 -0
  30. rootmemory/main.py +133 -0
  31. rootmemory/models/__init__.py +22 -0
  32. rootmemory/models/agent.py +30 -0
  33. rootmemory/models/audit.py +51 -0
  34. rootmemory/models/belief.py +39 -0
  35. rootmemory/models/claim.py +52 -0
  36. rootmemory/models/decision.py +38 -0
  37. rootmemory/models/edge.py +43 -0
  38. rootmemory/models/enums.py +104 -0
  39. rootmemory/models/node_ref.py +42 -0
  40. rootmemory/models/observation.py +51 -0
  41. rootmemory/py.typed +0 -0
  42. rootmemory/repositories/__init__.py +19 -0
  43. rootmemory/repositories/agent_repo.py +44 -0
  44. rootmemory/repositories/audit_repo.py +43 -0
  45. rootmemory/repositories/belief_repo.py +63 -0
  46. rootmemory/repositories/claim_repo.py +102 -0
  47. rootmemory/repositories/decision_repo.py +76 -0
  48. rootmemory/repositories/edge_repo.py +77 -0
  49. rootmemory/repositories/observation_repo.py +76 -0
  50. rootmemory/schemas/__init__.py +52 -0
  51. rootmemory/schemas/agent.py +26 -0
  52. rootmemory/schemas/belief.py +49 -0
  53. rootmemory/schemas/claim.py +54 -0
  54. rootmemory/schemas/common.py +27 -0
  55. rootmemory/schemas/observation.py +69 -0
  56. rootmemory/schemas/promotion.py +50 -0
  57. rootmemory/schemas/provenance.py +51 -0
  58. rootmemory/services/__init__.py +35 -0
  59. rootmemory/services/belief_service.py +161 -0
  60. rootmemory/services/contradiction_service.py +148 -0
  61. rootmemory/services/decision_service.py +65 -0
  62. rootmemory/services/graph_service.py +316 -0
  63. rootmemory/services/independence_service.py +159 -0
  64. rootmemory/services/invalidation_service.py +164 -0
  65. rootmemory/services/normalization_service.py +234 -0
  66. rootmemory/services/promotion_service.py +318 -0
  67. rootmemory/services/provenance_service.py +282 -0
  68. rootmemory/services/scoring_service.py +53 -0
  69. rootmemory/static/index.html +866 -0
  70. rootmemory-0.1.0.dist-info/METADATA +266 -0
  71. rootmemory-0.1.0.dist-info/RECORD +75 -0
  72. rootmemory-0.1.0.dist-info/WHEEL +5 -0
  73. rootmemory-0.1.0.dist-info/entry_points.txt +2 -0
  74. rootmemory-0.1.0.dist-info/licenses/LICENSE +21 -0
  75. rootmemory-0.1.0.dist-info/top_level.txt +1 -0
rootmemory/cli.py ADDED
@@ -0,0 +1,96 @@
1
+ """The ``rootmemory`` command.
2
+
3
+ rootmemory version
4
+ rootmemory demo run the four scenarios against a naive baseline
5
+ rootmemory serve start the API and the portal
6
+ rootmemory policy show the promotion policy in force
7
+
8
+ Kept deliberately thin: the API and the demo are optional extras, so this
9
+ imports them lazily and explains what to install if they are missing.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import argparse
15
+ import sys
16
+
17
+
18
+ def _missing(extra: str, what: str) -> int:
19
+ print(f"{what} needs the optional '{extra}' extra:", file=sys.stderr)
20
+ print(f' pip install "rootmemory[{extra}]"', file=sys.stderr)
21
+ return 1
22
+
23
+
24
+ def cmd_version() -> int:
25
+ from rootmemory import __version__
26
+
27
+ print(f"rootmemory {__version__}")
28
+ return 0
29
+
30
+
31
+ def cmd_policy() -> int:
32
+ import json
33
+
34
+ from rootmemory.services.promotion_service import PromotionPolicy
35
+
36
+ print(json.dumps(PromotionPolicy.from_settings().as_dict(), indent=2))
37
+ return 0
38
+
39
+
40
+ def cmd_serve(host: str, port: int, reload: bool) -> int:
41
+ try:
42
+ import uvicorn
43
+ except ModuleNotFoundError:
44
+ return _missing("api", "serve")
45
+
46
+ uvicorn.run("rootmemory.main:app", host=host, port=port, reload=reload)
47
+ return 0
48
+
49
+
50
+ def cmd_demo() -> int:
51
+ try:
52
+ from experiments.run_demo import main as run_demo
53
+ except ModuleNotFoundError:
54
+ print(
55
+ "The demo scenarios ship with the source repository, not the wheel.\n"
56
+ "Clone it to run them:\n"
57
+ " git clone https://github.com/Shubs5758/RootMemory",
58
+ file=sys.stderr,
59
+ )
60
+ return 1
61
+ return int(run_demo([]))
62
+
63
+
64
+ def main(argv: list[str] | None = None) -> int:
65
+ parser = argparse.ArgumentParser(
66
+ prog="rootmemory",
67
+ description="Provenance-aware memory for multi-agent AI systems.",
68
+ )
69
+ sub = parser.add_subparsers(dest="command")
70
+
71
+ sub.add_parser("version", help="print the installed version")
72
+ sub.add_parser("policy", help="print the promotion policy in force")
73
+ sub.add_parser("demo", help="run the demo scenarios (source checkout only)")
74
+
75
+ serve = sub.add_parser("serve", help="start the API and the portal at /ui")
76
+ serve.add_argument("--host", default="127.0.0.1")
77
+ serve.add_argument("--port", type=int, default=8000)
78
+ serve.add_argument("--reload", action="store_true")
79
+
80
+ args = parser.parse_args(argv)
81
+
82
+ if args.command == "version":
83
+ return cmd_version()
84
+ if args.command == "policy":
85
+ return cmd_policy()
86
+ if args.command == "demo":
87
+ return cmd_demo()
88
+ if args.command == "serve":
89
+ return cmd_serve(args.host, args.port, args.reload)
90
+
91
+ parser.print_help()
92
+ return 0
93
+
94
+
95
+ if __name__ == "__main__":
96
+ raise SystemExit(main())
rootmemory/client.py ADDED
@@ -0,0 +1,219 @@
1
+ """RootMemory: the interface agents use (spec section 48).
2
+
3
+ Agents never touch the database or the ORM. They go through this façade, which
4
+ keeps the memory layer framework-agnostic: the same client works for a plain
5
+ Python agent, a LangGraph node or a CrewAI worker.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import uuid
11
+ from collections.abc import Sequence
12
+
13
+ from sqlalchemy.orm import Session
14
+
15
+ from rootmemory.errors import NodeNotFoundError
16
+ from rootmemory.models.agent import Agent
17
+ from rootmemory.models.belief import Belief
18
+ from rootmemory.models.claim import Claim
19
+ from rootmemory.models.decision import Decision
20
+ from rootmemory.models.enums import EpistemicStatus, NodeType, ValidityStatus
21
+ from rootmemory.models.node_ref import NodeRef
22
+ from rootmemory.models.observation import Observation
23
+ from rootmemory.repositories.agent_repo import AgentRepository
24
+ from rootmemory.repositories.claim_repo import ClaimRepository
25
+ from rootmemory.repositories.decision_repo import DecisionRepository
26
+ from rootmemory.repositories.observation_repo import ObservationRepository
27
+ from rootmemory.services.belief_service import BeliefService, ParentSpec
28
+ from rootmemory.services.contradiction_service import ClaimContext, ContradictionService
29
+ from rootmemory.services.decision_service import DecisionService
30
+ from rootmemory.services.invalidation_service import InvalidationService, RepairSummary
31
+ from rootmemory.services.promotion_service import (
32
+ PromotionEvaluation,
33
+ PromotionPolicy,
34
+ PromotionService,
35
+ )
36
+ from rootmemory.services.provenance_service import ProvenanceService
37
+
38
+
39
+ class RootMemory:
40
+ """A thin, typed API over the memory services."""
41
+
42
+ def __init__(self, session: Session, policy: PromotionPolicy | None = None) -> None:
43
+ self.session = session
44
+ self.agents = AgentRepository(session)
45
+ self.observations = ObservationRepository(session)
46
+ self.claims = ClaimRepository(session)
47
+ self.decisions = DecisionRepository(session)
48
+ self.decision_service = DecisionService(session)
49
+ self.beliefs = BeliefService(session)
50
+ self.provenance = ProvenanceService(session)
51
+ self.contradictions = ContradictionService(session)
52
+ self.promotion = PromotionService(session, policy)
53
+ self.invalidation = InvalidationService(session, policy)
54
+
55
+ # ------------------------------------------------------------------
56
+ # agents
57
+ # ------------------------------------------------------------------
58
+
59
+ def register_agent(
60
+ self, name: str, role: str = "", description: str = "", trust_score: float = 0.5
61
+ ) -> Agent:
62
+ existing = self.agents.get_by_name(name)
63
+ if existing is not None:
64
+ return existing
65
+ return self.agents.create(
66
+ name=name, role=role, description=description, trust_score=trust_score
67
+ )
68
+
69
+ # ------------------------------------------------------------------
70
+ # evidence
71
+ # ------------------------------------------------------------------
72
+
73
+ def create_observation(
74
+ self,
75
+ source_type: str,
76
+ content: str,
77
+ source_uri: str | None = None,
78
+ source_actor: str | None = None,
79
+ created_by_agent_id: uuid.UUID | None = None,
80
+ reliability_score: float = 1.0,
81
+ source_family_id: str | None = None,
82
+ meta: dict | None = None,
83
+ ) -> Observation:
84
+ """Record a piece of external evidence."""
85
+ return self.observations.create(
86
+ source_type=source_type,
87
+ content=content,
88
+ source_uri=source_uri,
89
+ source_actor=source_actor,
90
+ created_by_agent_id=created_by_agent_id,
91
+ reliability_score=reliability_score,
92
+ source_family_id=source_family_id,
93
+ meta=meta or {},
94
+ )
95
+
96
+ def invalidate_observation(self, observation_id: uuid.UUID, reason: str) -> RepairSummary:
97
+ """Retract a source and repair everything downstream."""
98
+ return self.invalidation.invalidate_observation(observation_id, reason)
99
+
100
+ # ------------------------------------------------------------------
101
+ # beliefs
102
+ # ------------------------------------------------------------------
103
+
104
+ def create_belief(
105
+ self,
106
+ agent_id: uuid.UUID,
107
+ claim_text: str,
108
+ claim_key: str | None,
109
+ confidence: float,
110
+ derived_from: Sequence[Observation | Belief],
111
+ epistemic_status: EpistemicStatus = EpistemicStatus.INFERRED,
112
+ llm_generated: bool = False,
113
+ meta: dict | None = None,
114
+ ) -> Belief:
115
+ """Form a belief from observations and/or other beliefs.
116
+
117
+ ``derived_from`` takes model instances so callers cannot forget to say
118
+ which type each parent is.
119
+
120
+ ``claim_key`` may be None, in which case the configured normalizer
121
+ works out which proposition this belief is about.
122
+ """
123
+ parents = [ParentSpec(id=node.id, type=_node_type_of(node)) for node in derived_from]
124
+ return self.beliefs.create_belief(
125
+ agent_id=agent_id,
126
+ claim_text=claim_text,
127
+ normalized_claim_key=claim_key,
128
+ confidence=confidence,
129
+ parents=parents,
130
+ epistemic_status=epistemic_status,
131
+ llm_generated=llm_generated,
132
+ meta=meta,
133
+ )
134
+
135
+ def get_belief(self, belief_id: uuid.UUID) -> Belief:
136
+ belief = self.beliefs.beliefs.get(belief_id)
137
+ if belief is None:
138
+ raise NodeNotFoundError(f"belief {belief_id} does not exist")
139
+ return belief
140
+
141
+ def find_belief(self, belief_id: uuid.UUID) -> Belief | None:
142
+ """Look a belief up without raising when it is absent."""
143
+ return self.beliefs.beliefs.get(belief_id)
144
+
145
+ def list_beliefs(self) -> Sequence[Belief]:
146
+ """Every belief, oldest first."""
147
+ return self.beliefs.beliefs.list()
148
+
149
+ def propose_shared_belief(self, belief_id: uuid.UUID) -> Belief:
150
+ """Submit a private belief as a candidate for shared memory."""
151
+ return self.beliefs.submit_for_promotion(belief_id)
152
+
153
+ def evidence_roots(self, belief: Belief) -> list[Observation]:
154
+ """The observations a belief ultimately rests on."""
155
+ return self.provenance.find_evidence_root_observations(NodeRef.belief(belief.id))
156
+
157
+ # ------------------------------------------------------------------
158
+ # claims
159
+ # ------------------------------------------------------------------
160
+
161
+ def get_or_create_claim(self, claim_key: str, canonical_text: str) -> Claim:
162
+ return self.claims.get_or_create(claim_key, canonical_text)
163
+
164
+ def support_claim(self, claim_id: uuid.UUID, belief_id: uuid.UUID) -> None:
165
+ self.contradictions.register_support(claim_id, belief_id)
166
+
167
+ def contradict_claim(self, claim_id: uuid.UUID, belief_id: uuid.UUID) -> None:
168
+ self.contradictions.register_contradiction(claim_id, belief_id)
169
+
170
+ def get_claim_context(self, claim_id: uuid.UUID) -> ClaimContext:
171
+ """Read a claim the honest way: both sides, with independence counts."""
172
+ return self.contradictions.build_context(claim_id)
173
+
174
+ def evaluate_claim(self, claim_id: uuid.UUID) -> PromotionEvaluation:
175
+ """Run the promotion gate without changing the claim."""
176
+ return self.promotion.evaluate(claim_id)
177
+
178
+ def promote_claim(self, claim_id: uuid.UUID) -> PromotionEvaluation:
179
+ """Attempt promotion into shared memory. Always re-evaluates."""
180
+ return self.promotion.promote(claim_id)
181
+
182
+ # ------------------------------------------------------------------
183
+ # decisions
184
+ # ------------------------------------------------------------------
185
+
186
+ def create_decision(
187
+ self,
188
+ agent_id: uuid.UUID,
189
+ decision_type: str,
190
+ content: str,
191
+ depends_on_claims: Sequence[Claim] = (),
192
+ meta: dict | None = None,
193
+ ) -> Decision:
194
+ """Record a decision and the claims it rests on."""
195
+ return self.decision_service.create_decision(
196
+ agent_id=agent_id,
197
+ decision_type=decision_type,
198
+ content=content,
199
+ claim_ids=[claim.id for claim in depends_on_claims],
200
+ meta=meta,
201
+ )
202
+
203
+ # ------------------------------------------------------------------
204
+ # misc
205
+ # ------------------------------------------------------------------
206
+
207
+ def observation_status(self, observation_id: uuid.UUID) -> ValidityStatus:
208
+ observation = self.observations.get(observation_id)
209
+ if observation is None:
210
+ raise NodeNotFoundError(f"observation {observation_id} does not exist")
211
+ return ValidityStatus(observation.validity_status)
212
+
213
+
214
+ def _node_type_of(node: Observation | Belief) -> NodeType:
215
+ if isinstance(node, Observation):
216
+ return NodeType.OBSERVATION
217
+ if isinstance(node, Belief):
218
+ return NodeType.BELIEF
219
+ raise TypeError(f"a belief cannot be derived from {type(node).__name__}")
rootmemory/config.py ADDED
@@ -0,0 +1,64 @@
1
+ """Application configuration.
2
+
3
+ Every knob that changes epistemic behaviour (the promotion policy) is exposed
4
+ here so experiments can vary it without touching service code.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from functools import lru_cache
10
+ from typing import Literal
11
+
12
+ from pydantic import Field
13
+ from pydantic_settings import BaseSettings, SettingsConfigDict
14
+
15
+
16
+ class Settings(BaseSettings):
17
+ """Runtime settings, read from the environment and an optional .env file."""
18
+
19
+ model_config = SettingsConfigDict(env_file=".env", extra="ignore")
20
+
21
+ # Default is SQLite so the demo, the tests and a fresh clone all run with no
22
+ # infrastructure. docker-compose / .env point this at PostgreSQL.
23
+ database_url: str = "sqlite+pysqlite:///./rootmemory.db"
24
+ sql_echo: bool = False
25
+ log_level: str = "INFO"
26
+
27
+ promotion_min_independent_support: int = Field(default=2, ge=1)
28
+ promotion_min_confidence: float = Field(default=0.75, ge=0.0, le=1.0)
29
+ promotion_max_high_confidence_contradictions: int = Field(default=0, ge=0)
30
+ promotion_contradiction_confidence_threshold: float = Field(default=0.75, ge=0.0, le=1.0)
31
+ promotion_policy_version: str = "v1"
32
+
33
+ # Comma-separated. Empty means authentication is disabled, which is the
34
+ # right default for local development and the wrong one on a network.
35
+ api_keys: str = ""
36
+
37
+ # Claim normalization: how free-text beliefs are mapped onto a claim key.
38
+ # "manual" - the caller always supplies claim_key (V1 behaviour)
39
+ # "deterministic" - derive a slug from the text, no LLM
40
+ # "llm" - ask a model to match an existing claim, with the
41
+ # deterministic slug as the fallback
42
+ claim_normalization: Literal["manual", "deterministic", "llm"] = "manual"
43
+
44
+ openai_api_key: str | None = None
45
+ openai_model: str | None = None
46
+ openai_base_url: str | None = None
47
+
48
+ @property
49
+ def is_sqlite(self) -> bool:
50
+ return self.database_url.startswith("sqlite")
51
+
52
+ @property
53
+ def api_key_list(self) -> list[str]:
54
+ return [key.strip() for key in self.api_keys.split(",") if key.strip()]
55
+
56
+ @property
57
+ def auth_enabled(self) -> bool:
58
+ """Authentication is on exactly when at least one key is configured."""
59
+ return bool(self.api_key_list)
60
+
61
+
62
+ @lru_cache
63
+ def get_settings() -> Settings:
64
+ return Settings()
File without changes
rootmemory/db/base.py ADDED
@@ -0,0 +1,42 @@
1
+ """Declarative base and shared column helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import uuid
6
+ from datetime import UTC, datetime
7
+
8
+ from sqlalchemy import JSON, DateTime, MetaData, Uuid
9
+ from sqlalchemy.orm import DeclarativeBase, mapped_column
10
+
11
+ NAMING_CONVENTION = {
12
+ "ix": "ix_%(column_0_label)s",
13
+ "uq": "uq_%(table_name)s_%(column_0_name)s",
14
+ "ck": "ck_%(table_name)s_%(constraint_name)s",
15
+ "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
16
+ "pk": "pk_%(table_name)s",
17
+ }
18
+
19
+
20
+ class Base(DeclarativeBase):
21
+ """Base class for every ORM model."""
22
+
23
+ metadata = MetaData(naming_convention=NAMING_CONVENTION)
24
+
25
+
26
+ def utcnow() -> datetime:
27
+ """Timezone-aware 'now'. Used as a Python-side default so behaviour is
28
+ identical on SQLite and PostgreSQL."""
29
+ return datetime.now(UTC)
30
+
31
+
32
+ def pk_column(): # type: ignore[no-untyped-def]
33
+ """UUID primary key, generated client-side so callers know the id up front."""
34
+ return mapped_column(Uuid(as_uuid=True), primary_key=True, default=uuid.uuid4)
35
+
36
+
37
+ def timestamp_column(**kwargs): # type: ignore[no-untyped-def]
38
+ return mapped_column(DateTime(timezone=True), default=utcnow, **kwargs)
39
+
40
+
41
+ def json_column(**kwargs): # type: ignore[no-untyped-def]
42
+ return mapped_column(JSON, default=dict, **kwargs)
@@ -0,0 +1,59 @@
1
+ """Alembic environment.
2
+
3
+ The database URL comes from the application settings, so migrations and the
4
+ running app can never drift apart.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from logging.config import fileConfig
10
+
11
+ from alembic import context
12
+ from sqlalchemy import engine_from_config, pool
13
+
14
+ import rootmemory.models # noqa: F401 (register every mapper before autogenerate)
15
+ from rootmemory.config import get_settings
16
+ from rootmemory.db.base import Base
17
+
18
+ config = context.config
19
+ config.set_main_option("sqlalchemy.url", get_settings().database_url)
20
+
21
+ if config.config_file_name is not None:
22
+ fileConfig(config.config_file_name)
23
+
24
+ target_metadata = Base.metadata
25
+
26
+
27
+ def run_migrations_offline() -> None:
28
+ context.configure(
29
+ url=config.get_main_option("sqlalchemy.url"),
30
+ target_metadata=target_metadata,
31
+ literal_binds=True,
32
+ dialect_opts={"paramstyle": "named"},
33
+ render_as_batch=True,
34
+ )
35
+ with context.begin_transaction():
36
+ context.run_migrations()
37
+
38
+
39
+ def run_migrations_online() -> None:
40
+ connectable = engine_from_config(
41
+ config.get_section(config.config_ini_section, {}),
42
+ prefix="sqlalchemy.",
43
+ poolclass=pool.NullPool,
44
+ )
45
+ with connectable.connect() as connection:
46
+ context.configure(
47
+ connection=connection,
48
+ target_metadata=target_metadata,
49
+ # Needed for SQLite, harmless on PostgreSQL.
50
+ render_as_batch=True,
51
+ )
52
+ with context.begin_transaction():
53
+ context.run_migrations()
54
+
55
+
56
+ if context.is_offline_mode():
57
+ run_migrations_offline()
58
+ else:
59
+ run_migrations_online()
@@ -0,0 +1,26 @@
1
+ """${message}
2
+
3
+ Revision ID: ${up_revision}
4
+ Revises: ${down_revision | comma,n}
5
+ Create Date: ${create_date}
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from collections.abc import Sequence
10
+
11
+ from alembic import op
12
+ import sqlalchemy as sa
13
+ ${imports if imports else ""}
14
+
15
+ revision: str = ${repr(up_revision)}
16
+ down_revision: str | None = ${repr(down_revision)}
17
+ branch_labels: str | Sequence[str] | None = ${repr(branch_labels)}
18
+ depends_on: str | Sequence[str] | None = ${repr(depends_on)}
19
+
20
+
21
+ def upgrade() -> None:
22
+ ${upgrades if upgrades else "pass"}
23
+
24
+
25
+ def downgrade() -> None:
26
+ ${downgrades if downgrades else "pass"}