shellbrain 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.
- app/__init__.py +1 -0
- app/__main__.py +7 -0
- app/boot/__init__.py +1 -0
- app/boot/admin_db.py +88 -0
- app/boot/config.py +14 -0
- app/boot/create_policy.py +52 -0
- app/boot/db.py +70 -0
- app/boot/embeddings.py +55 -0
- app/boot/home.py +45 -0
- app/boot/migrations.py +61 -0
- app/boot/read_policy.py +179 -0
- app/boot/repos.py +15 -0
- app/boot/retrieval.py +3 -0
- app/boot/thresholds.py +19 -0
- app/boot/update_policy.py +34 -0
- app/boot/use_cases.py +22 -0
- app/config/__init__.py +1 -0
- app/config/defaults/create_policy.yaml +7 -0
- app/config/defaults/read_policy.yaml +25 -0
- app/config/defaults/runtime.yaml +10 -0
- app/config/defaults/thresholds.yaml +3 -0
- app/config/defaults/update_policy.yaml +5 -0
- app/config/loader.py +58 -0
- app/core/__init__.py +1 -0
- app/core/contracts/__init__.py +1 -0
- app/core/contracts/errors.py +29 -0
- app/core/contracts/requests.py +211 -0
- app/core/contracts/responses.py +15 -0
- app/core/entities/__init__.py +1 -0
- app/core/entities/associations.py +58 -0
- app/core/entities/episodes.py +66 -0
- app/core/entities/evidence.py +29 -0
- app/core/entities/facts.py +30 -0
- app/core/entities/guidance.py +47 -0
- app/core/entities/identity.py +48 -0
- app/core/entities/memory.py +34 -0
- app/core/entities/runtime_context.py +19 -0
- app/core/entities/session_state.py +31 -0
- app/core/entities/telemetry.py +152 -0
- app/core/entities/utility.py +14 -0
- app/core/interfaces/__init__.py +1 -0
- app/core/interfaces/clock.py +12 -0
- app/core/interfaces/config.py +28 -0
- app/core/interfaces/embeddings.py +12 -0
- app/core/interfaces/idgen.py +11 -0
- app/core/interfaces/repos.py +279 -0
- app/core/interfaces/retrieval.py +20 -0
- app/core/interfaces/session_state_store.py +33 -0
- app/core/interfaces/unit_of_work.py +50 -0
- app/core/policies/__init__.py +1 -0
- app/core/policies/_shared/__init__.py +1 -0
- app/core/policies/_shared/executor.py +132 -0
- app/core/policies/_shared/side_effects.py +9 -0
- app/core/policies/create_policy/__init__.py +1 -0
- app/core/policies/create_policy/pipeline.py +96 -0
- app/core/policies/read_policy/__init__.py +1 -0
- app/core/policies/read_policy/bm25.py +114 -0
- app/core/policies/read_policy/context_pack_builder.py +140 -0
- app/core/policies/read_policy/expansion.py +132 -0
- app/core/policies/read_policy/fusion_rrf.py +34 -0
- app/core/policies/read_policy/lexical_query.py +101 -0
- app/core/policies/read_policy/pipeline.py +93 -0
- app/core/policies/read_policy/scenario_lift.py +11 -0
- app/core/policies/read_policy/scoring.py +61 -0
- app/core/policies/read_policy/seed_retrieval.py +54 -0
- app/core/policies/read_policy/utility_prior.py +11 -0
- app/core/policies/update_policy/__init__.py +1 -0
- app/core/policies/update_policy/pipeline.py +80 -0
- app/core/use_cases/__init__.py +1 -0
- app/core/use_cases/build_guidance.py +85 -0
- app/core/use_cases/create_memory.py +26 -0
- app/core/use_cases/manage_session_state.py +159 -0
- app/core/use_cases/read_memory.py +21 -0
- app/core/use_cases/record_episode_sync_telemetry.py +19 -0
- app/core/use_cases/record_operation_telemetry.py +32 -0
- app/core/use_cases/sync_episode.py +162 -0
- app/core/use_cases/update_memory.py +40 -0
- app/migrations/__init__.py +1 -0
- app/migrations/env.py +65 -0
- app/migrations/versions/20260226_0001_initial_schema.py +232 -0
- app/migrations/versions/20260312_0002_add_hard_invariants.py +60 -0
- app/migrations/versions/20260312_0003_drop_create_confidence.py +40 -0
- app/migrations/versions/20260313_0004_episode_sync_hardening.py +71 -0
- app/migrations/versions/20260313_0005_evidence_episode_event_refs.py +45 -0
- app/migrations/versions/20260318_0006_usage_telemetry_schema.py +175 -0
- app/migrations/versions/20260319_0007_identity_session_guidance.py +49 -0
- app/migrations/versions/20260320_0008_instance_metadata_and_backup_safety.py +31 -0
- app/migrations/versions/__init__.py +1 -0
- app/periphery/__init__.py +1 -0
- app/periphery/admin/__init__.py +1 -0
- app/periphery/admin/backup.py +360 -0
- app/periphery/admin/destructive_guard.py +32 -0
- app/periphery/admin/doctor.py +192 -0
- app/periphery/admin/init.py +996 -0
- app/periphery/admin/instance_guard.py +211 -0
- app/periphery/admin/machine_state.py +354 -0
- app/periphery/admin/privileges.py +42 -0
- app/periphery/admin/repo_state.py +266 -0
- app/periphery/admin/restore.py +30 -0
- app/periphery/cli/__init__.py +1 -0
- app/periphery/cli/handlers.py +830 -0
- app/periphery/cli/hydration.py +119 -0
- app/periphery/cli/main.py +710 -0
- app/periphery/cli/presenter_json.py +10 -0
- app/periphery/cli/schema_validation.py +201 -0
- app/periphery/db/__init__.py +1 -0
- app/periphery/db/engine.py +10 -0
- app/periphery/db/models/__init__.py +1 -0
- app/periphery/db/models/associations.py +55 -0
- app/periphery/db/models/episodes.py +55 -0
- app/periphery/db/models/evidence.py +19 -0
- app/periphery/db/models/experiences.py +33 -0
- app/periphery/db/models/instance_metadata.py +17 -0
- app/periphery/db/models/memories.py +39 -0
- app/periphery/db/models/metadata.py +6 -0
- app/periphery/db/models/registry.py +18 -0
- app/periphery/db/models/telemetry.py +174 -0
- app/periphery/db/models/utility.py +19 -0
- app/periphery/db/models/views.py +154 -0
- app/periphery/db/repos/__init__.py +1 -0
- app/periphery/db/repos/relational/__init__.py +1 -0
- app/periphery/db/repos/relational/associations_repo.py +117 -0
- app/periphery/db/repos/relational/episodes_repo.py +188 -0
- app/periphery/db/repos/relational/evidence_repo.py +82 -0
- app/periphery/db/repos/relational/experiences_repo.py +41 -0
- app/periphery/db/repos/relational/memories_repo.py +99 -0
- app/periphery/db/repos/relational/read_policy_repo.py +202 -0
- app/periphery/db/repos/relational/telemetry_repo.py +161 -0
- app/periphery/db/repos/relational/utility_repo.py +30 -0
- app/periphery/db/repos/semantic/__init__.py +1 -0
- app/periphery/db/repos/semantic/keyword_retrieval_repo.py +63 -0
- app/periphery/db/repos/semantic/semantic_retrieval_repo.py +111 -0
- app/periphery/db/session.py +10 -0
- app/periphery/db/uow.py +75 -0
- app/periphery/embeddings/__init__.py +1 -0
- app/periphery/embeddings/local_provider.py +35 -0
- app/periphery/embeddings/query_vector_search.py +18 -0
- app/periphery/episodes/__init__.py +1 -0
- app/periphery/episodes/claude_code.py +387 -0
- app/periphery/episodes/codex.py +423 -0
- app/periphery/episodes/launcher.py +66 -0
- app/periphery/episodes/normalization.py +31 -0
- app/periphery/episodes/poller.py +299 -0
- app/periphery/episodes/source_discovery.py +66 -0
- app/periphery/episodes/tool_filter.py +165 -0
- app/periphery/identity/__init__.py +1 -0
- app/periphery/identity/claude_hook_install.py +67 -0
- app/periphery/identity/claude_runtime.py +83 -0
- app/periphery/identity/codex_runtime.py +32 -0
- app/periphery/identity/compatibility.py +38 -0
- app/periphery/identity/resolver.py +163 -0
- app/periphery/session_state/__init__.py +1 -0
- app/periphery/session_state/file_store.py +100 -0
- app/periphery/telemetry/__init__.py +33 -0
- app/periphery/telemetry/operation_summary.py +299 -0
- app/periphery/telemetry/session_selection.py +156 -0
- app/periphery/telemetry/sync_summary.py +65 -0
- app/periphery/validation/__init__.py +1 -0
- app/periphery/validation/integrity_validation.py +253 -0
- app/periphery/validation/semantic_validation.py +94 -0
- shellbrain-0.1.0.dist-info/METADATA +130 -0
- shellbrain-0.1.0.dist-info/RECORD +165 -0
- shellbrain-0.1.0.dist-info/WHEEL +5 -0
- shellbrain-0.1.0.dist-info/entry_points.txt +2 -0
- shellbrain-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""This module defines SQLAlchemy Core tables for utility observation records."""
|
|
2
|
+
|
|
3
|
+
from sqlalchemy import CheckConstraint, Column, Float, ForeignKey, String, Table
|
|
4
|
+
from sqlalchemy.dialects.postgresql import TIMESTAMP
|
|
5
|
+
|
|
6
|
+
from app.periphery.db.models.metadata import metadata
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
utility_observations = Table(
|
|
10
|
+
"utility_observations",
|
|
11
|
+
metadata,
|
|
12
|
+
Column("id", String, primary_key=True),
|
|
13
|
+
Column("memory_id", String, ForeignKey("memories.id", ondelete="CASCADE"), nullable=False),
|
|
14
|
+
Column("problem_id", String, ForeignKey("memories.id", ondelete="CASCADE"), nullable=False),
|
|
15
|
+
Column("vote", Float, nullable=False),
|
|
16
|
+
Column("rationale", String),
|
|
17
|
+
Column("created_at", TIMESTAMP(timezone=True), nullable=False),
|
|
18
|
+
CheckConstraint("vote >= -1 AND vote <= 1", name="ck_utility_observations_vote_range"),
|
|
19
|
+
)
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""This module defines view SQL strings used to build derived read-model views."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
CURRENT_FACT_SNAPSHOT_SQL = """
|
|
5
|
+
CREATE OR REPLACE VIEW current_fact_snapshot AS
|
|
6
|
+
SELECT m.*
|
|
7
|
+
FROM memories m
|
|
8
|
+
WHERE m.kind = 'fact'
|
|
9
|
+
AND m.archived = FALSE
|
|
10
|
+
AND NOT EXISTS (
|
|
11
|
+
SELECT 1 FROM fact_updates fu WHERE fu.old_fact_id = m.id
|
|
12
|
+
);
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
GLOBAL_UTILITY_SQL = """
|
|
17
|
+
CREATE OR REPLACE VIEW global_utility AS
|
|
18
|
+
SELECT memory_id, AVG(vote) AS utility_mean, COUNT(*) AS observations
|
|
19
|
+
FROM utility_observations
|
|
20
|
+
GROUP BY memory_id;
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
USAGE_COMMAND_DAILY_SQL = """
|
|
25
|
+
CREATE OR REPLACE VIEW usage_command_daily AS
|
|
26
|
+
SELECT
|
|
27
|
+
repo_id,
|
|
28
|
+
date_trunc('day', created_at AT TIME ZONE 'UTC') AS day_utc,
|
|
29
|
+
command,
|
|
30
|
+
outcome,
|
|
31
|
+
COUNT(*)::INTEGER AS invocation_count,
|
|
32
|
+
AVG(total_latency_ms)::DOUBLE PRECISION AS avg_latency_ms
|
|
33
|
+
FROM operation_invocations
|
|
34
|
+
GROUP BY repo_id, date_trunc('day', created_at AT TIME ZONE 'UTC'), command, outcome;
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
USAGE_MEMORY_RETRIEVAL_SQL = """
|
|
39
|
+
CREATE OR REPLACE VIEW usage_memory_retrieval AS
|
|
40
|
+
SELECT
|
|
41
|
+
oi.repo_id,
|
|
42
|
+
rri.memory_id,
|
|
43
|
+
rri.kind,
|
|
44
|
+
rri.section,
|
|
45
|
+
COUNT(*)::INTEGER AS retrieval_count,
|
|
46
|
+
MAX(oi.created_at) AS last_seen_at
|
|
47
|
+
FROM read_result_items rri
|
|
48
|
+
JOIN operation_invocations oi ON oi.id = rri.invocation_id
|
|
49
|
+
GROUP BY oi.repo_id, rri.memory_id, rri.kind, rri.section;
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
USAGE_WRITE_EFFECTS_SQL = """
|
|
54
|
+
CREATE OR REPLACE VIEW usage_write_effects AS
|
|
55
|
+
SELECT
|
|
56
|
+
wei.repo_id,
|
|
57
|
+
date_trunc('day', oi.created_at AT TIME ZONE 'UTC') AS day_utc,
|
|
58
|
+
wei.effect_type,
|
|
59
|
+
COUNT(*)::INTEGER AS effect_count
|
|
60
|
+
FROM write_effect_items wei
|
|
61
|
+
JOIN operation_invocations oi ON oi.id = wei.invocation_id
|
|
62
|
+
GROUP BY wei.repo_id, date_trunc('day', oi.created_at AT TIME ZONE 'UTC'), wei.effect_type;
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
USAGE_SYNC_HEALTH_SQL = """
|
|
67
|
+
CREATE OR REPLACE VIEW usage_sync_health AS
|
|
68
|
+
WITH sync_grouped AS (
|
|
69
|
+
SELECT
|
|
70
|
+
repo_id,
|
|
71
|
+
host_app,
|
|
72
|
+
date_trunc('day', created_at AT TIME ZONE 'UTC') AS day_utc,
|
|
73
|
+
COUNT(*)::INTEGER AS sync_run_count,
|
|
74
|
+
COUNT(*) FILTER (WHERE outcome = 'error')::INTEGER AS failed_sync_count,
|
|
75
|
+
COALESCE(SUM(imported_event_count), 0)::INTEGER AS imported_event_count
|
|
76
|
+
FROM episode_sync_runs
|
|
77
|
+
GROUP BY repo_id, host_app, date_trunc('day', created_at AT TIME ZONE 'UTC')
|
|
78
|
+
),
|
|
79
|
+
tool_grouped AS (
|
|
80
|
+
SELECT
|
|
81
|
+
esr.repo_id,
|
|
82
|
+
esr.host_app,
|
|
83
|
+
date_trunc('day', esr.created_at AT TIME ZONE 'UTC') AS day_utc,
|
|
84
|
+
estt.tool_type,
|
|
85
|
+
SUM(estt.event_count)::INTEGER AS event_count
|
|
86
|
+
FROM episode_sync_tool_types estt
|
|
87
|
+
JOIN episode_sync_runs esr ON esr.id = estt.sync_run_id
|
|
88
|
+
GROUP BY esr.repo_id, esr.host_app, date_trunc('day', esr.created_at AT TIME ZONE 'UTC'), estt.tool_type
|
|
89
|
+
),
|
|
90
|
+
tool_objects AS (
|
|
91
|
+
SELECT
|
|
92
|
+
repo_id,
|
|
93
|
+
host_app,
|
|
94
|
+
day_utc,
|
|
95
|
+
jsonb_object_agg(tool_type, event_count ORDER BY tool_type) AS tool_type_counts
|
|
96
|
+
FROM tool_grouped
|
|
97
|
+
GROUP BY repo_id, host_app, day_utc
|
|
98
|
+
)
|
|
99
|
+
SELECT
|
|
100
|
+
sg.repo_id,
|
|
101
|
+
sg.host_app,
|
|
102
|
+
sg.day_utc,
|
|
103
|
+
sg.sync_run_count,
|
|
104
|
+
sg.failed_sync_count,
|
|
105
|
+
sg.imported_event_count,
|
|
106
|
+
COALESCE(toj.tool_type_counts, '{}'::jsonb) AS tool_type_counts
|
|
107
|
+
FROM sync_grouped sg
|
|
108
|
+
LEFT JOIN tool_objects toj
|
|
109
|
+
ON toj.repo_id = sg.repo_id
|
|
110
|
+
AND toj.host_app = sg.host_app
|
|
111
|
+
AND toj.day_utc = sg.day_utc;
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
USAGE_SESSION_PROTOCOL_SQL = """
|
|
116
|
+
CREATE OR REPLACE VIEW usage_session_protocol AS
|
|
117
|
+
SELECT
|
|
118
|
+
oi.repo_id,
|
|
119
|
+
oi.selected_thread_id,
|
|
120
|
+
COUNT(*) FILTER (WHERE oi.command = 'read')::INTEGER AS read_count,
|
|
121
|
+
COUNT(*) FILTER (WHERE oi.command = 'events')::INTEGER AS events_count,
|
|
122
|
+
COUNT(*) FILTER (WHERE oi.command IN ('create', 'update'))::INTEGER AS write_count,
|
|
123
|
+
COUNT(*) FILTER (
|
|
124
|
+
WHERE oi.command = 'read'
|
|
125
|
+
AND COALESCE(ris.zero_results, FALSE)
|
|
126
|
+
)::INTEGER AS zero_result_read_count,
|
|
127
|
+
COUNT(*) FILTER (WHERE oi.selection_ambiguous)::INTEGER AS ambiguous_selection_count,
|
|
128
|
+
COUNT(*) FILTER (
|
|
129
|
+
WHERE oi.command IN ('create', 'update')
|
|
130
|
+
AND EXISTS (
|
|
131
|
+
SELECT 1
|
|
132
|
+
FROM operation_invocations prior_events
|
|
133
|
+
WHERE prior_events.repo_id = oi.repo_id
|
|
134
|
+
AND prior_events.selected_thread_id = oi.selected_thread_id
|
|
135
|
+
AND prior_events.command = 'events'
|
|
136
|
+
AND prior_events.created_at < oi.created_at
|
|
137
|
+
)
|
|
138
|
+
)::INTEGER AS writes_preceded_by_events_count,
|
|
139
|
+
COUNT(*) FILTER (
|
|
140
|
+
WHERE oi.command = 'events'
|
|
141
|
+
AND NOT EXISTS (
|
|
142
|
+
SELECT 1
|
|
143
|
+
FROM operation_invocations later_writes
|
|
144
|
+
WHERE later_writes.repo_id = oi.repo_id
|
|
145
|
+
AND later_writes.selected_thread_id = oi.selected_thread_id
|
|
146
|
+
AND later_writes.command IN ('create', 'update')
|
|
147
|
+
AND later_writes.created_at > oi.created_at
|
|
148
|
+
)
|
|
149
|
+
)::INTEGER AS events_without_following_write_count
|
|
150
|
+
FROM operation_invocations oi
|
|
151
|
+
LEFT JOIN read_invocation_summaries ris ON ris.invocation_id = oi.id
|
|
152
|
+
WHERE oi.selected_thread_id IS NOT NULL
|
|
153
|
+
GROUP BY oi.repo_id, oi.selected_thread_id;
|
|
154
|
+
"""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""This package groups relational and semantic repository implementations."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""This package contains aggregate-oriented relational repository classes."""
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""This module defines relational repository operations for association structures."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
|
|
5
|
+
from sqlalchemy import case, select, update
|
|
6
|
+
|
|
7
|
+
from app.core.entities.associations import AssociationEdge, AssociationObservation, AssociationSourceMode, AssociationState
|
|
8
|
+
from app.core.interfaces.repos import IAssociationsRepo
|
|
9
|
+
from app.periphery.db.models.associations import association_edges, association_observations
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AssociationsRepo(IAssociationsRepo):
|
|
13
|
+
"""This class provides persistence operations for association edges and observations."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, session) -> None:
|
|
16
|
+
"""This method stores the active DB session for repository operations."""
|
|
17
|
+
|
|
18
|
+
self._session = session
|
|
19
|
+
|
|
20
|
+
def upsert_edge(self, edge: AssociationEdge) -> AssociationEdge:
|
|
21
|
+
"""This method inserts or updates an association edge and returns the stored edge."""
|
|
22
|
+
|
|
23
|
+
existing = (
|
|
24
|
+
self._session.execute(
|
|
25
|
+
select(association_edges).where(
|
|
26
|
+
association_edges.c.repo_id == edge.repo_id,
|
|
27
|
+
association_edges.c.from_memory_id == edge.from_memory_id,
|
|
28
|
+
association_edges.c.to_memory_id == edge.to_memory_id,
|
|
29
|
+
association_edges.c.relation_type == edge.relation_type.value,
|
|
30
|
+
)
|
|
31
|
+
)
|
|
32
|
+
.mappings()
|
|
33
|
+
.first()
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
now = datetime.now(timezone.utc)
|
|
37
|
+
if existing is None:
|
|
38
|
+
self._session.execute(
|
|
39
|
+
association_edges.insert().values(
|
|
40
|
+
id=edge.id,
|
|
41
|
+
repo_id=edge.repo_id,
|
|
42
|
+
from_memory_id=edge.from_memory_id,
|
|
43
|
+
to_memory_id=edge.to_memory_id,
|
|
44
|
+
relation_type=edge.relation_type.value,
|
|
45
|
+
source_mode=edge.source_mode.value,
|
|
46
|
+
state=edge.state.value,
|
|
47
|
+
strength=edge.strength,
|
|
48
|
+
obs_count=0,
|
|
49
|
+
positive_obs=0,
|
|
50
|
+
negative_obs=0,
|
|
51
|
+
salience_sum=0.0,
|
|
52
|
+
created_at=now,
|
|
53
|
+
updated_at=now,
|
|
54
|
+
)
|
|
55
|
+
)
|
|
56
|
+
return edge
|
|
57
|
+
|
|
58
|
+
source_mode = existing["source_mode"]
|
|
59
|
+
if source_mode != edge.source_mode.value and source_mode != "mixed":
|
|
60
|
+
source_mode = "mixed"
|
|
61
|
+
strength = max(float(existing["strength"]), edge.strength)
|
|
62
|
+
self._session.execute(
|
|
63
|
+
update(association_edges)
|
|
64
|
+
.where(association_edges.c.id == existing["id"])
|
|
65
|
+
.values(
|
|
66
|
+
source_mode=source_mode,
|
|
67
|
+
state=edge.state.value,
|
|
68
|
+
strength=strength,
|
|
69
|
+
updated_at=now,
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
return AssociationEdge(
|
|
73
|
+
id=existing["id"],
|
|
74
|
+
repo_id=edge.repo_id,
|
|
75
|
+
from_memory_id=edge.from_memory_id,
|
|
76
|
+
to_memory_id=edge.to_memory_id,
|
|
77
|
+
relation_type=edge.relation_type,
|
|
78
|
+
source_mode=AssociationSourceMode(source_mode),
|
|
79
|
+
state=AssociationState(edge.state.value),
|
|
80
|
+
strength=strength,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
def append_observation(self, observation: AssociationObservation) -> None:
|
|
84
|
+
"""This method appends an immutable association observation row."""
|
|
85
|
+
|
|
86
|
+
now = datetime.now(timezone.utc)
|
|
87
|
+
self._session.execute(
|
|
88
|
+
association_observations.insert().values(
|
|
89
|
+
id=observation.id,
|
|
90
|
+
repo_id=observation.repo_id,
|
|
91
|
+
edge_id=observation.edge_id,
|
|
92
|
+
from_memory_id=observation.from_memory_id,
|
|
93
|
+
to_memory_id=observation.to_memory_id,
|
|
94
|
+
relation_type=observation.relation_type.value,
|
|
95
|
+
source=observation.source,
|
|
96
|
+
problem_id=observation.problem_id,
|
|
97
|
+
episode_id=observation.episode_id,
|
|
98
|
+
valence=observation.valence,
|
|
99
|
+
salience=observation.salience,
|
|
100
|
+
created_at=now,
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
if observation.edge_id:
|
|
104
|
+
self._session.execute(
|
|
105
|
+
update(association_edges)
|
|
106
|
+
.where(association_edges.c.id == observation.edge_id)
|
|
107
|
+
.values(
|
|
108
|
+
obs_count=association_edges.c.obs_count + 1,
|
|
109
|
+
positive_obs=association_edges.c.positive_obs
|
|
110
|
+
+ case((observation.valence > 0, 1), else_=0),
|
|
111
|
+
negative_obs=association_edges.c.negative_obs
|
|
112
|
+
+ case((observation.valence < 0, 1), else_=0),
|
|
113
|
+
salience_sum=association_edges.c.salience_sum + observation.salience,
|
|
114
|
+
last_reinforced_at=now,
|
|
115
|
+
updated_at=now,
|
|
116
|
+
)
|
|
117
|
+
)
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""This module defines relational repository operations for episodic provenance tables."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from typing import Sequence
|
|
5
|
+
|
|
6
|
+
from sqlalchemy import func, select, update
|
|
7
|
+
|
|
8
|
+
from app.core.entities.episodes import Episode, EpisodeEvent, EpisodeEventSource, EpisodeStatus, SessionTransfer
|
|
9
|
+
from app.core.interfaces.repos import IEpisodesRepo
|
|
10
|
+
from app.periphery.db.models.episodes import episode_events, episodes, session_transfers
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class EpisodesRepo(IEpisodesRepo):
|
|
14
|
+
"""This class provides persistence operations for episodes, events, and transfers."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, session) -> None:
|
|
17
|
+
"""This method stores the active DB session for repository operations."""
|
|
18
|
+
|
|
19
|
+
self._session = session
|
|
20
|
+
|
|
21
|
+
def create_episode(self, episode: Episode) -> None:
|
|
22
|
+
"""This method persists an episode row."""
|
|
23
|
+
|
|
24
|
+
self._session.execute(
|
|
25
|
+
episodes.insert().values(
|
|
26
|
+
id=episode.id,
|
|
27
|
+
repo_id=episode.repo_id,
|
|
28
|
+
host_app=episode.host_app,
|
|
29
|
+
thread_id=episode.thread_id,
|
|
30
|
+
title=episode.title,
|
|
31
|
+
objective=episode.objective,
|
|
32
|
+
status=episode.status.value,
|
|
33
|
+
started_at=episode.started_at or datetime.now(timezone.utc),
|
|
34
|
+
ended_at=episode.ended_at,
|
|
35
|
+
created_at=episode.created_at or datetime.now(timezone.utc),
|
|
36
|
+
)
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def get_episode_by_thread(
|
|
40
|
+
self,
|
|
41
|
+
*,
|
|
42
|
+
repo_id: str,
|
|
43
|
+
thread_id: str,
|
|
44
|
+
) -> Episode | None:
|
|
45
|
+
"""This method fetches one episode by canonical host session key."""
|
|
46
|
+
|
|
47
|
+
row = (
|
|
48
|
+
self._session.execute(
|
|
49
|
+
select(episodes).where(
|
|
50
|
+
episodes.c.repo_id == repo_id,
|
|
51
|
+
episodes.c.thread_id == thread_id,
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
.mappings()
|
|
55
|
+
.first()
|
|
56
|
+
)
|
|
57
|
+
if row is None:
|
|
58
|
+
return None
|
|
59
|
+
return Episode(
|
|
60
|
+
id=row["id"],
|
|
61
|
+
repo_id=row["repo_id"],
|
|
62
|
+
host_app=row["host_app"],
|
|
63
|
+
thread_id=row["thread_id"],
|
|
64
|
+
title=row["title"],
|
|
65
|
+
objective=row["objective"],
|
|
66
|
+
status=EpisodeStatus(row["status"]),
|
|
67
|
+
started_at=row["started_at"],
|
|
68
|
+
ended_at=row["ended_at"],
|
|
69
|
+
created_at=row["created_at"],
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def list_event_keys(self, *, episode_id: str) -> list[str]:
|
|
73
|
+
"""This method returns already-imported upstream event keys for one episode."""
|
|
74
|
+
|
|
75
|
+
rows = self._session.execute(
|
|
76
|
+
select(episode_events.c.host_event_key).where(episode_events.c.episode_id == episode_id)
|
|
77
|
+
).scalars()
|
|
78
|
+
return [str(value) for value in rows]
|
|
79
|
+
|
|
80
|
+
def next_event_seq(self, *, episode_id: str) -> int:
|
|
81
|
+
"""This method returns the next append sequence number for one episode."""
|
|
82
|
+
|
|
83
|
+
max_seq = self._session.execute(
|
|
84
|
+
select(func.max(episode_events.c.seq)).where(episode_events.c.episode_id == episode_id)
|
|
85
|
+
).scalar_one()
|
|
86
|
+
return 1 if max_seq is None else int(max_seq) + 1
|
|
87
|
+
|
|
88
|
+
def append_event(self, event: EpisodeEvent) -> None:
|
|
89
|
+
"""This method appends an episode event row."""
|
|
90
|
+
|
|
91
|
+
self._session.execute(
|
|
92
|
+
episode_events.insert().values(
|
|
93
|
+
id=event.id,
|
|
94
|
+
episode_id=event.episode_id,
|
|
95
|
+
seq=event.seq,
|
|
96
|
+
host_event_key=event.host_event_key,
|
|
97
|
+
source=event.source.value,
|
|
98
|
+
content=event.content,
|
|
99
|
+
created_at=event.created_at or datetime.now(timezone.utc),
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def close_episode(self, *, episode_id: str, ended_at: datetime) -> None:
|
|
104
|
+
"""This method marks an active episode closed."""
|
|
105
|
+
|
|
106
|
+
self._session.execute(
|
|
107
|
+
update(episodes)
|
|
108
|
+
.where(episodes.c.id == episode_id)
|
|
109
|
+
.values(status="closed", ended_at=ended_at)
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def append_transfer(self, transfer: SessionTransfer) -> None:
|
|
113
|
+
"""This method appends a session transfer row."""
|
|
114
|
+
|
|
115
|
+
self._session.execute(
|
|
116
|
+
session_transfers.insert().values(
|
|
117
|
+
id=transfer.id,
|
|
118
|
+
repo_id=transfer.repo_id,
|
|
119
|
+
from_episode_id=transfer.from_episode_id,
|
|
120
|
+
to_episode_id=transfer.to_episode_id,
|
|
121
|
+
event_id=transfer.event_id,
|
|
122
|
+
transfer_kind=transfer.transfer_kind,
|
|
123
|
+
rationale=transfer.rationale,
|
|
124
|
+
transferred_by=transfer.transferred_by,
|
|
125
|
+
created_at=transfer.created_at or datetime.now(timezone.utc),
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
def list_existing_event_ids(self, *, event_ids: Sequence[str]) -> list[str]:
|
|
130
|
+
"""This method returns stored event ids regardless of repo visibility."""
|
|
131
|
+
|
|
132
|
+
if not event_ids:
|
|
133
|
+
return []
|
|
134
|
+
rows = self._session.execute(
|
|
135
|
+
select(episode_events.c.id).where(episode_events.c.id.in_(event_ids))
|
|
136
|
+
).scalars()
|
|
137
|
+
return [str(value) for value in rows]
|
|
138
|
+
|
|
139
|
+
def list_visible_event_ids(self, *, repo_id: str, event_ids: Sequence[str]) -> list[str]:
|
|
140
|
+
"""This method returns stored event ids visible to one repo."""
|
|
141
|
+
|
|
142
|
+
if not event_ids:
|
|
143
|
+
return []
|
|
144
|
+
rows = self._session.execute(
|
|
145
|
+
select(episode_events.c.id)
|
|
146
|
+
.select_from(episode_events.join(episodes, episode_events.c.episode_id == episodes.c.id))
|
|
147
|
+
.where(
|
|
148
|
+
episodes.c.repo_id == repo_id,
|
|
149
|
+
episode_events.c.id.in_(event_ids),
|
|
150
|
+
)
|
|
151
|
+
).scalars()
|
|
152
|
+
return [str(value) for value in rows]
|
|
153
|
+
|
|
154
|
+
def list_recent_events(
|
|
155
|
+
self,
|
|
156
|
+
*,
|
|
157
|
+
repo_id: str,
|
|
158
|
+
episode_id: str,
|
|
159
|
+
limit: int,
|
|
160
|
+
) -> list[EpisodeEvent]:
|
|
161
|
+
"""This method returns recent events for one repo-visible episode ordered newest first."""
|
|
162
|
+
|
|
163
|
+
rows = (
|
|
164
|
+
self._session.execute(
|
|
165
|
+
select(episode_events)
|
|
166
|
+
.select_from(episode_events.join(episodes, episode_events.c.episode_id == episodes.c.id))
|
|
167
|
+
.where(
|
|
168
|
+
episodes.c.repo_id == repo_id,
|
|
169
|
+
episode_events.c.episode_id == episode_id,
|
|
170
|
+
)
|
|
171
|
+
.order_by(episode_events.c.seq.desc())
|
|
172
|
+
.limit(limit)
|
|
173
|
+
)
|
|
174
|
+
.mappings()
|
|
175
|
+
.all()
|
|
176
|
+
)
|
|
177
|
+
return [
|
|
178
|
+
EpisodeEvent(
|
|
179
|
+
id=row["id"],
|
|
180
|
+
episode_id=row["episode_id"],
|
|
181
|
+
seq=row["seq"],
|
|
182
|
+
host_event_key=row["host_event_key"],
|
|
183
|
+
source=EpisodeEventSource(row["source"]),
|
|
184
|
+
content=row["content"],
|
|
185
|
+
created_at=row["created_at"],
|
|
186
|
+
)
|
|
187
|
+
for row in rows
|
|
188
|
+
]
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""This module defines relational repository operations for evidence references and links."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from uuid import uuid4
|
|
5
|
+
|
|
6
|
+
from sqlalchemy import select, update
|
|
7
|
+
from sqlalchemy.dialects.postgresql import insert
|
|
8
|
+
|
|
9
|
+
from app.core.entities.evidence import EvidenceRef
|
|
10
|
+
from app.core.interfaces.repos import IEvidenceRepo
|
|
11
|
+
from app.periphery.db.models.associations import association_edge_evidence
|
|
12
|
+
from app.periphery.db.models.evidence import evidence_refs
|
|
13
|
+
from app.periphery.db.models.memories import memory_evidence
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class EvidenceRepo(IEvidenceRepo):
|
|
17
|
+
"""This class provides persistence operations for evidence references and links."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, session) -> None:
|
|
20
|
+
"""This method stores the active DB session for repository operations."""
|
|
21
|
+
|
|
22
|
+
self._session = session
|
|
23
|
+
|
|
24
|
+
def upsert_ref(self, repo_id: str, ref: str) -> EvidenceRef:
|
|
25
|
+
"""This method inserts or returns a canonical evidence reference row."""
|
|
26
|
+
|
|
27
|
+
existing = (
|
|
28
|
+
self._session.execute(
|
|
29
|
+
select(evidence_refs).where(
|
|
30
|
+
evidence_refs.c.repo_id == repo_id,
|
|
31
|
+
(evidence_refs.c.episode_event_id == ref) | (evidence_refs.c.ref == ref),
|
|
32
|
+
)
|
|
33
|
+
)
|
|
34
|
+
.mappings()
|
|
35
|
+
.first()
|
|
36
|
+
)
|
|
37
|
+
if existing:
|
|
38
|
+
if existing["episode_event_id"] is None:
|
|
39
|
+
self._session.execute(
|
|
40
|
+
update(evidence_refs)
|
|
41
|
+
.where(evidence_refs.c.id == existing["id"])
|
|
42
|
+
.values(episode_event_id=ref, ref=ref)
|
|
43
|
+
)
|
|
44
|
+
existing = dict(existing)
|
|
45
|
+
existing["episode_event_id"] = ref
|
|
46
|
+
existing["ref"] = ref
|
|
47
|
+
return EvidenceRef(
|
|
48
|
+
id=existing["id"],
|
|
49
|
+
repo_id=existing["repo_id"],
|
|
50
|
+
ref=existing["ref"],
|
|
51
|
+
episode_event_id=existing["episode_event_id"],
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
evidence_id = str(uuid4())
|
|
55
|
+
self._session.execute(
|
|
56
|
+
evidence_refs.insert().values(
|
|
57
|
+
id=evidence_id,
|
|
58
|
+
repo_id=repo_id,
|
|
59
|
+
ref=ref,
|
|
60
|
+
episode_event_id=ref,
|
|
61
|
+
created_at=datetime.now(timezone.utc),
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
return EvidenceRef(id=evidence_id, repo_id=repo_id, ref=ref, episode_event_id=ref)
|
|
65
|
+
|
|
66
|
+
def link_memory_evidence(self, memory_id: str, evidence_id: str) -> None:
|
|
67
|
+
"""This method creates shellbrain-to-evidence link rows."""
|
|
68
|
+
|
|
69
|
+
self._session.execute(
|
|
70
|
+
insert(memory_evidence)
|
|
71
|
+
.values(memory_id=memory_id, evidence_id=evidence_id)
|
|
72
|
+
.on_conflict_do_nothing(index_elements=["memory_id", "evidence_id"])
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def link_association_edge_evidence(self, edge_id: str, evidence_id: str) -> None:
|
|
76
|
+
"""This method creates association-edge-to-evidence link rows."""
|
|
77
|
+
|
|
78
|
+
self._session.execute(
|
|
79
|
+
insert(association_edge_evidence)
|
|
80
|
+
.values(edge_id=edge_id, evidence_id=evidence_id)
|
|
81
|
+
.on_conflict_do_nothing(index_elements=["edge_id", "evidence_id"])
|
|
82
|
+
)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""This module defines relational repository operations for experiential links."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
|
|
5
|
+
from app.core.entities.facts import FactUpdate, ProblemAttempt
|
|
6
|
+
from app.core.interfaces.repos import IExperiencesRepo
|
|
7
|
+
from app.periphery.db.models.experiences import fact_updates, problem_attempts
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ExperiencesRepo(IExperiencesRepo):
|
|
11
|
+
"""This class provides persistence operations for problem attempts and fact updates."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, session) -> None:
|
|
14
|
+
"""This method stores the active DB session for repository operations."""
|
|
15
|
+
|
|
16
|
+
self._session = session
|
|
17
|
+
|
|
18
|
+
def create_problem_attempt(self, attempt: ProblemAttempt) -> None:
|
|
19
|
+
"""This method persists a problem-attempt link row."""
|
|
20
|
+
|
|
21
|
+
self._session.execute(
|
|
22
|
+
problem_attempts.insert().values(
|
|
23
|
+
problem_id=attempt.problem_id,
|
|
24
|
+
attempt_id=attempt.attempt_id,
|
|
25
|
+
role=attempt.role.value,
|
|
26
|
+
created_at=datetime.now(timezone.utc),
|
|
27
|
+
)
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
def create_fact_update(self, fact_update: FactUpdate) -> None:
|
|
31
|
+
"""This method persists a fact-update chain row."""
|
|
32
|
+
|
|
33
|
+
self._session.execute(
|
|
34
|
+
fact_updates.insert().values(
|
|
35
|
+
id=fact_update.id,
|
|
36
|
+
old_fact_id=fact_update.old_fact_id,
|
|
37
|
+
change_id=fact_update.change_id,
|
|
38
|
+
new_fact_id=fact_update.new_fact_id,
|
|
39
|
+
created_at=datetime.now(timezone.utc),
|
|
40
|
+
)
|
|
41
|
+
)
|