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,26 @@
|
|
|
1
|
+
"""This module defines the create-shellbrain use-case orchestration entry point."""
|
|
2
|
+
|
|
3
|
+
from uuid import uuid4
|
|
4
|
+
|
|
5
|
+
from app.core.contracts.requests import MemoryCreateRequest
|
|
6
|
+
from app.core.contracts.responses import OperationResult
|
|
7
|
+
from app.core.interfaces.embeddings import IEmbeddingProvider
|
|
8
|
+
from app.core.interfaces.unit_of_work import IUnitOfWork
|
|
9
|
+
from app.core.policies.create_policy.pipeline import apply_create_plan, build_create_plan
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def execute_create_memory(
|
|
13
|
+
request: MemoryCreateRequest,
|
|
14
|
+
uow: IUnitOfWork,
|
|
15
|
+
*,
|
|
16
|
+
embedding_provider: IEmbeddingProvider,
|
|
17
|
+
embedding_model: str,
|
|
18
|
+
) -> OperationResult:
|
|
19
|
+
"""This function orchestrates create flow for an already-validated request."""
|
|
20
|
+
|
|
21
|
+
memory_id = str(uuid4())
|
|
22
|
+
payload = request.model_dump(mode="python")
|
|
23
|
+
payload["memory_id"] = memory_id
|
|
24
|
+
plan = build_create_plan(payload, embedding_model=embedding_model)
|
|
25
|
+
apply_create_plan(plan, uow, embedding_provider=embedding_provider)
|
|
26
|
+
return OperationResult(status="ok", data={"memory_id": memory_id, "planned_side_effects": plan})
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Core working-session lifecycle rules for per-caller session state."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import replace
|
|
6
|
+
from datetime import datetime, timedelta, timezone
|
|
7
|
+
|
|
8
|
+
from app.core.entities.identity import CallerIdentity, IdentityTrustLevel
|
|
9
|
+
from app.core.entities.session_state import SessionState
|
|
10
|
+
from app.core.interfaces.clock import IClock
|
|
11
|
+
from app.core.interfaces.session_state_store import ISessionStateStore
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
IDLE_EXPIRY = timedelta(hours=24)
|
|
15
|
+
GC_EXPIRY = timedelta(days=7)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SessionStateManager:
|
|
19
|
+
"""Application service for loading, touching, and mutating working session state."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, *, store: ISessionStateStore, clock: IClock | None = None) -> None:
|
|
22
|
+
"""Capture the persistence store and optional clock dependency."""
|
|
23
|
+
|
|
24
|
+
self._store = store
|
|
25
|
+
self._clock = clock
|
|
26
|
+
|
|
27
|
+
def load_active_state(self, *, repo_root, caller_identity: CallerIdentity | None) -> SessionState | None:
|
|
28
|
+
"""Load and touch the active caller state when identity is trusted."""
|
|
29
|
+
|
|
30
|
+
if caller_identity is None or caller_identity.trust_level != IdentityTrustLevel.TRUSTED:
|
|
31
|
+
return None
|
|
32
|
+
now_iso = self._now_iso()
|
|
33
|
+
state = self._store.load(repo_root=repo_root, caller_id=caller_identity.canonical_id or "")
|
|
34
|
+
if state is None:
|
|
35
|
+
state = SessionState(
|
|
36
|
+
caller_id=caller_identity.canonical_id or "",
|
|
37
|
+
host_app=caller_identity.host_app,
|
|
38
|
+
host_session_key=caller_identity.host_session_key,
|
|
39
|
+
agent_key=caller_identity.agent_key,
|
|
40
|
+
session_started_at=now_iso,
|
|
41
|
+
last_seen_at=now_iso,
|
|
42
|
+
)
|
|
43
|
+
else:
|
|
44
|
+
state = self.reset_if_idle(state, now_iso=now_iso)
|
|
45
|
+
state.last_seen_at = now_iso
|
|
46
|
+
self._store.save(repo_root=repo_root, state=state)
|
|
47
|
+
return state
|
|
48
|
+
|
|
49
|
+
def record_events(
|
|
50
|
+
self,
|
|
51
|
+
*,
|
|
52
|
+
repo_root,
|
|
53
|
+
caller_identity: CallerIdentity | None,
|
|
54
|
+
episode_id: str,
|
|
55
|
+
event_ids: list[str],
|
|
56
|
+
) -> SessionState | None:
|
|
57
|
+
"""Persist the latest events context for one trusted caller."""
|
|
58
|
+
|
|
59
|
+
state = self.load_active_state(repo_root=repo_root, caller_identity=caller_identity)
|
|
60
|
+
if state is None:
|
|
61
|
+
return None
|
|
62
|
+
now_iso = self._now_iso()
|
|
63
|
+
state.last_events_episode_id = episode_id
|
|
64
|
+
state.last_events_event_ids = list(event_ids)
|
|
65
|
+
state.last_events_at = now_iso
|
|
66
|
+
state.last_seen_at = now_iso
|
|
67
|
+
self._store.save(repo_root=repo_root, state=state)
|
|
68
|
+
return state
|
|
69
|
+
|
|
70
|
+
def record_problem(
|
|
71
|
+
self,
|
|
72
|
+
*,
|
|
73
|
+
repo_root,
|
|
74
|
+
caller_identity: CallerIdentity | None,
|
|
75
|
+
problem_id: str,
|
|
76
|
+
) -> SessionState | None:
|
|
77
|
+
"""Persist the active problem for one trusted caller."""
|
|
78
|
+
|
|
79
|
+
state = self.load_active_state(repo_root=repo_root, caller_identity=caller_identity)
|
|
80
|
+
if state is None:
|
|
81
|
+
return None
|
|
82
|
+
state.current_problem_id = problem_id
|
|
83
|
+
state.last_seen_at = self._now_iso()
|
|
84
|
+
self._store.save(repo_root=repo_root, state=state)
|
|
85
|
+
return state
|
|
86
|
+
|
|
87
|
+
def record_guidance(
|
|
88
|
+
self,
|
|
89
|
+
*,
|
|
90
|
+
repo_root,
|
|
91
|
+
caller_identity: CallerIdentity | None,
|
|
92
|
+
problem_id: str,
|
|
93
|
+
) -> SessionState | None:
|
|
94
|
+
"""Persist the latest emitted guidance timestamp for one trusted caller."""
|
|
95
|
+
|
|
96
|
+
state = self.load_active_state(repo_root=repo_root, caller_identity=caller_identity)
|
|
97
|
+
if state is None:
|
|
98
|
+
return None
|
|
99
|
+
now_iso = self._now_iso()
|
|
100
|
+
state.last_guidance_problem_id = problem_id
|
|
101
|
+
state.last_guidance_at = now_iso
|
|
102
|
+
state.last_seen_at = now_iso
|
|
103
|
+
self._store.save(repo_root=repo_root, state=state)
|
|
104
|
+
return state
|
|
105
|
+
|
|
106
|
+
def clear_for_caller(self, *, repo_root, caller_id: str) -> None:
|
|
107
|
+
"""Delete one caller state."""
|
|
108
|
+
|
|
109
|
+
self._store.delete(repo_root=repo_root, caller_id=caller_id)
|
|
110
|
+
|
|
111
|
+
def garbage_collect(self, *, repo_root) -> list[str]:
|
|
112
|
+
"""Delete states older than the configured GC threshold."""
|
|
113
|
+
|
|
114
|
+
cutoff_iso = (self._now() - GC_EXPIRY).isoformat()
|
|
115
|
+
return self._store.gc(repo_root=repo_root, older_than_iso=cutoff_iso)
|
|
116
|
+
|
|
117
|
+
@staticmethod
|
|
118
|
+
def reset_if_idle(state: SessionState, *, now_iso: str) -> SessionState:
|
|
119
|
+
"""Reset working-session fields when the caller has been idle too long."""
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
last_seen = _parse_iso(state.last_seen_at)
|
|
123
|
+
except ValueError:
|
|
124
|
+
last_seen = datetime.min.replace(tzinfo=timezone.utc)
|
|
125
|
+
now = _parse_iso(now_iso)
|
|
126
|
+
if now - last_seen < IDLE_EXPIRY:
|
|
127
|
+
return state
|
|
128
|
+
return replace(
|
|
129
|
+
state,
|
|
130
|
+
session_started_at=now_iso,
|
|
131
|
+
last_seen_at=now_iso,
|
|
132
|
+
current_problem_id=None,
|
|
133
|
+
last_events_episode_id=None,
|
|
134
|
+
last_events_event_ids=[],
|
|
135
|
+
last_events_at=None,
|
|
136
|
+
last_guidance_at=None,
|
|
137
|
+
last_guidance_problem_id=None,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
def _now(self) -> datetime:
|
|
141
|
+
"""Return the current UTC time using the injected clock when present."""
|
|
142
|
+
|
|
143
|
+
if self._clock is not None:
|
|
144
|
+
return self._clock.now()
|
|
145
|
+
return datetime.now(timezone.utc)
|
|
146
|
+
|
|
147
|
+
def _now_iso(self) -> str:
|
|
148
|
+
"""Return the current UTC time in ISO-8601 form."""
|
|
149
|
+
|
|
150
|
+
return self._now().isoformat()
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _parse_iso(value: str) -> datetime:
|
|
154
|
+
"""Parse one ISO timestamp into a timezone-aware datetime."""
|
|
155
|
+
|
|
156
|
+
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
157
|
+
if parsed.tzinfo is None:
|
|
158
|
+
return parsed.replace(tzinfo=timezone.utc)
|
|
159
|
+
return parsed.astimezone(timezone.utc)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""This module defines the read-shellbrain use-case orchestration entry point."""
|
|
2
|
+
|
|
3
|
+
from app.core.contracts.requests import MemoryReadRequest
|
|
4
|
+
from app.core.contracts.responses import OperationResult
|
|
5
|
+
from app.core.interfaces.unit_of_work import IUnitOfWork
|
|
6
|
+
from app.core.policies.read_policy.pipeline import build_context_pack
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def execute_read_memory(request: MemoryReadRequest, uow: IUnitOfWork) -> OperationResult:
|
|
10
|
+
"""This function orchestrates read flow with retrieval and context-pack policy hooks."""
|
|
11
|
+
|
|
12
|
+
payload = request.model_dump(mode="python")
|
|
13
|
+
context_pack = build_context_pack(
|
|
14
|
+
payload,
|
|
15
|
+
keyword_retrieval=uow.keyword_retrieval,
|
|
16
|
+
memories=uow.memories,
|
|
17
|
+
semantic_retrieval=uow.semantic_retrieval,
|
|
18
|
+
read_policy=uow.read_policy,
|
|
19
|
+
vector_search=uow.vector_search,
|
|
20
|
+
)
|
|
21
|
+
return OperationResult(status="ok", data={"pack": context_pack})
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Thin orchestration for sync-run telemetry writes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
|
|
7
|
+
from app.core.entities.telemetry import EpisodeSyncRunRecord, EpisodeSyncToolTypeRecord
|
|
8
|
+
from app.core.interfaces.unit_of_work import IUnitOfWork
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def record_episode_sync_telemetry(
|
|
12
|
+
*,
|
|
13
|
+
uow: IUnitOfWork,
|
|
14
|
+
run: EpisodeSyncRunRecord,
|
|
15
|
+
tool_types: Sequence[EpisodeSyncToolTypeRecord] = (),
|
|
16
|
+
) -> None:
|
|
17
|
+
"""Persist one sync-run row and its per-tool aggregates."""
|
|
18
|
+
|
|
19
|
+
uow.telemetry.insert_episode_sync_run(run, tuple(tool_types))
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Thin orchestration for operation-level telemetry writes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
|
|
7
|
+
from app.core.entities.telemetry import (
|
|
8
|
+
OperationInvocationRecord,
|
|
9
|
+
ReadResultItemRecord,
|
|
10
|
+
ReadSummaryRecord,
|
|
11
|
+
WriteEffectItemRecord,
|
|
12
|
+
WriteSummaryRecord,
|
|
13
|
+
)
|
|
14
|
+
from app.core.interfaces.unit_of_work import IUnitOfWork
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def record_operation_telemetry(
|
|
18
|
+
*,
|
|
19
|
+
uow: IUnitOfWork,
|
|
20
|
+
invocation: OperationInvocationRecord,
|
|
21
|
+
read_summary: ReadSummaryRecord | None = None,
|
|
22
|
+
read_items: Sequence[ReadResultItemRecord] = (),
|
|
23
|
+
write_summary: WriteSummaryRecord | None = None,
|
|
24
|
+
write_items: Sequence[WriteEffectItemRecord] = (),
|
|
25
|
+
) -> None:
|
|
26
|
+
"""Persist one invocation row and any attached read or write summaries."""
|
|
27
|
+
|
|
28
|
+
uow.telemetry.insert_operation_invocation(invocation)
|
|
29
|
+
if read_summary is not None:
|
|
30
|
+
uow.telemetry.insert_read_summary(read_summary, tuple(read_items))
|
|
31
|
+
if write_summary is not None:
|
|
32
|
+
uow.telemetry.insert_write_summary(write_summary, tuple(write_items))
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""Synchronize normalized host transcript events into the episodic provenance tables."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
import json
|
|
8
|
+
from typing import Any
|
|
9
|
+
from uuid import uuid4
|
|
10
|
+
|
|
11
|
+
from app.core.entities.episodes import Episode, EpisodeEvent, EpisodeEventSource, EpisodeStatus
|
|
12
|
+
from app.core.interfaces.unit_of_work import IUnitOfWork
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def sync_episode(
|
|
16
|
+
*,
|
|
17
|
+
repo_id: str,
|
|
18
|
+
host_app: str,
|
|
19
|
+
host_session_key: str,
|
|
20
|
+
thread_id: str,
|
|
21
|
+
transcript_path: str,
|
|
22
|
+
normalized_events: Sequence[dict[str, Any]],
|
|
23
|
+
uow: IUnitOfWork,
|
|
24
|
+
) -> dict[str, Any]:
|
|
25
|
+
"""Import one already-normalized host transcript into episodes and episode events."""
|
|
26
|
+
|
|
27
|
+
counts = _count_normalized_events(normalized_events)
|
|
28
|
+
episode = uow.episodes.get_episode_by_thread(repo_id=repo_id, thread_id=thread_id)
|
|
29
|
+
imported_count = 0
|
|
30
|
+
|
|
31
|
+
if episode is None:
|
|
32
|
+
started_at = _earliest_event_timestamp(normalized_events) or datetime.now(timezone.utc)
|
|
33
|
+
episode = Episode(
|
|
34
|
+
id=str(uuid4()),
|
|
35
|
+
repo_id=repo_id,
|
|
36
|
+
host_app=host_app,
|
|
37
|
+
thread_id=thread_id,
|
|
38
|
+
status=EpisodeStatus.ACTIVE,
|
|
39
|
+
started_at=started_at,
|
|
40
|
+
created_at=datetime.now(timezone.utc),
|
|
41
|
+
)
|
|
42
|
+
uow.episodes.create_episode(episode)
|
|
43
|
+
|
|
44
|
+
existing_keys = set(uow.episodes.list_event_keys(episode_id=episode.id))
|
|
45
|
+
next_seq = uow.episodes.next_event_seq(episode_id=episode.id)
|
|
46
|
+
for normalized_event in normalized_events:
|
|
47
|
+
host_event_key = str(normalized_event["host_event_key"])
|
|
48
|
+
if host_event_key in existing_keys:
|
|
49
|
+
continue
|
|
50
|
+
created_at = _parse_timestamp(str(normalized_event["occurred_at"]))
|
|
51
|
+
source = EpisodeEventSource(str(normalized_event["source"]))
|
|
52
|
+
uow.episodes.append_event(
|
|
53
|
+
EpisodeEvent(
|
|
54
|
+
id=str(uuid4()),
|
|
55
|
+
episode_id=episode.id,
|
|
56
|
+
seq=next_seq,
|
|
57
|
+
host_event_key=host_event_key,
|
|
58
|
+
source=source,
|
|
59
|
+
content=json.dumps(normalized_event),
|
|
60
|
+
created_at=created_at,
|
|
61
|
+
)
|
|
62
|
+
)
|
|
63
|
+
existing_keys.add(host_event_key)
|
|
64
|
+
next_seq += 1
|
|
65
|
+
imported_count += 1
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
"episode_id": episode.id,
|
|
69
|
+
"thread_id": thread_id,
|
|
70
|
+
"imported_event_count": imported_count,
|
|
71
|
+
"transcript_path": transcript_path,
|
|
72
|
+
"total_event_count": counts["total_event_count"],
|
|
73
|
+
"user_event_count": counts["user_event_count"],
|
|
74
|
+
"assistant_event_count": counts["assistant_event_count"],
|
|
75
|
+
"tool_event_count": counts["tool_event_count"],
|
|
76
|
+
"system_event_count": counts["system_event_count"],
|
|
77
|
+
"tool_type_counts": counts["tool_type_counts"],
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def sync_episode_from_host(
|
|
82
|
+
*,
|
|
83
|
+
repo_id: str,
|
|
84
|
+
host_app: str,
|
|
85
|
+
host_session_key: str,
|
|
86
|
+
uow: IUnitOfWork,
|
|
87
|
+
search_roots,
|
|
88
|
+
last_known_path=None,
|
|
89
|
+
) -> dict[str, Any]:
|
|
90
|
+
"""Backward-compatible host sync wrapper used by existing callers."""
|
|
91
|
+
|
|
92
|
+
from app.periphery.episodes.normalization import normalize_host_transcript
|
|
93
|
+
from app.periphery.episodes.source_discovery import resolve_host_transcript_source
|
|
94
|
+
|
|
95
|
+
transcript_path = resolve_host_transcript_source(
|
|
96
|
+
host_app=host_app,
|
|
97
|
+
host_session_key=host_session_key,
|
|
98
|
+
search_roots=search_roots,
|
|
99
|
+
last_known_path=last_known_path,
|
|
100
|
+
)
|
|
101
|
+
normalized_events = normalize_host_transcript(
|
|
102
|
+
host_app=host_app,
|
|
103
|
+
host_session_key=host_session_key,
|
|
104
|
+
transcript_path=transcript_path,
|
|
105
|
+
)
|
|
106
|
+
return sync_episode(
|
|
107
|
+
repo_id=repo_id,
|
|
108
|
+
host_app=host_app,
|
|
109
|
+
host_session_key=host_session_key,
|
|
110
|
+
thread_id=f"{host_app}:{host_session_key}",
|
|
111
|
+
transcript_path=str(transcript_path),
|
|
112
|
+
normalized_events=normalized_events,
|
|
113
|
+
uow=uow,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _parse_timestamp(value: str) -> datetime:
|
|
118
|
+
"""Parse one host timestamp into a timezone-aware datetime."""
|
|
119
|
+
|
|
120
|
+
if not value:
|
|
121
|
+
return datetime.now(timezone.utc)
|
|
122
|
+
return datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _earliest_event_timestamp(events: Sequence[dict[str, Any]]) -> datetime | None:
|
|
126
|
+
"""Return the earliest normalized event timestamp if any exist."""
|
|
127
|
+
|
|
128
|
+
timestamps = [
|
|
129
|
+
_parse_timestamp(str(event["occurred_at"]))
|
|
130
|
+
for event in events
|
|
131
|
+
if event.get("occurred_at")
|
|
132
|
+
]
|
|
133
|
+
if not timestamps:
|
|
134
|
+
return None
|
|
135
|
+
return min(timestamps)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _count_normalized_events(events: Sequence[dict[str, Any]]) -> dict[str, Any]:
|
|
139
|
+
"""Compute telemetry-friendly source and tool-type counts from normalized events."""
|
|
140
|
+
|
|
141
|
+
tool_type_counts: dict[str, int] = {}
|
|
142
|
+
counts = {
|
|
143
|
+
"total_event_count": len(events),
|
|
144
|
+
"user_event_count": 0,
|
|
145
|
+
"assistant_event_count": 0,
|
|
146
|
+
"tool_event_count": 0,
|
|
147
|
+
"system_event_count": 0,
|
|
148
|
+
"tool_type_counts": tool_type_counts,
|
|
149
|
+
}
|
|
150
|
+
for event in events:
|
|
151
|
+
source = str(event.get("source") or "")
|
|
152
|
+
if source == "user":
|
|
153
|
+
counts["user_event_count"] += 1
|
|
154
|
+
elif source == "assistant":
|
|
155
|
+
counts["assistant_event_count"] += 1
|
|
156
|
+
elif source == "tool":
|
|
157
|
+
counts["tool_event_count"] += 1
|
|
158
|
+
tool_name = str(event.get("tool_name") or "unknown_tool")
|
|
159
|
+
tool_type_counts[tool_name] = tool_type_counts.get(tool_name, 0) + 1
|
|
160
|
+
elif source == "system":
|
|
161
|
+
counts["system_event_count"] += 1
|
|
162
|
+
return counts
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""This module defines the update-shellbrain use-case orchestration entry point."""
|
|
2
|
+
|
|
3
|
+
from app.core.contracts.requests import MemoryBatchUpdateRequest, MemoryUpdateRequest
|
|
4
|
+
from app.core.contracts.responses import OperationResult
|
|
5
|
+
from app.core.interfaces.unit_of_work import IUnitOfWork
|
|
6
|
+
from app.core.policies.update_policy.pipeline import apply_update_plan, build_update_plan
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def execute_update_memory(request: MemoryUpdateRequest | MemoryBatchUpdateRequest, uow: IUnitOfWork) -> OperationResult:
|
|
10
|
+
"""This function orchestrates update flow for an already-validated request."""
|
|
11
|
+
|
|
12
|
+
if isinstance(request, MemoryBatchUpdateRequest):
|
|
13
|
+
plan: list[dict[str, object]] = []
|
|
14
|
+
updated_memory_ids: list[str] = []
|
|
15
|
+
for item in request.updates:
|
|
16
|
+
plan.extend(
|
|
17
|
+
build_update_plan(
|
|
18
|
+
{
|
|
19
|
+
"repo_id": request.repo_id,
|
|
20
|
+
"memory_id": item.memory_id,
|
|
21
|
+
"update": item.update.model_dump(mode="python"),
|
|
22
|
+
}
|
|
23
|
+
)
|
|
24
|
+
)
|
|
25
|
+
updated_memory_ids.append(item.memory_id)
|
|
26
|
+
apply_update_plan(plan, uow)
|
|
27
|
+
problem_id = request.updates[0].update.problem_id
|
|
28
|
+
return OperationResult(
|
|
29
|
+
status="ok",
|
|
30
|
+
data={
|
|
31
|
+
"problem_id": problem_id,
|
|
32
|
+
"updated_memory_ids": updated_memory_ids,
|
|
33
|
+
"applied_count": len(request.updates),
|
|
34
|
+
"planned_side_effects": plan,
|
|
35
|
+
},
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
plan = build_update_plan(request.model_dump(mode="python"))
|
|
39
|
+
apply_update_plan(plan, uow)
|
|
40
|
+
return OperationResult(status="ok", data={"memory_id": request.memory_id, "planned_side_effects": plan})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""This package contains Alembic migration configuration and revisions."""
|
app/migrations/env.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""This module defines the Alembic environment for online and offline PostgreSQL migrations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from logging.config import fileConfig
|
|
7
|
+
|
|
8
|
+
from alembic import context
|
|
9
|
+
from sqlalchemy import engine_from_config, pool
|
|
10
|
+
|
|
11
|
+
from app.periphery.db.models.registry import target_metadata
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
config = context.config
|
|
15
|
+
|
|
16
|
+
if config.config_file_name is not None:
|
|
17
|
+
fileConfig(config.config_file_name)
|
|
18
|
+
|
|
19
|
+
configured_url = config.get_main_option("sqlalchemy.url")
|
|
20
|
+
if not configured_url:
|
|
21
|
+
admin_dsn = os.getenv("SHELLBRAIN_DB_ADMIN_DSN")
|
|
22
|
+
app_dsn = os.getenv("SHELLBRAIN_DB_DSN")
|
|
23
|
+
if admin_dsn:
|
|
24
|
+
config.set_main_option("sqlalchemy.url", admin_dsn)
|
|
25
|
+
elif app_dsn:
|
|
26
|
+
config.set_main_option("sqlalchemy.url", app_dsn)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def run_migrations_offline() -> None:
|
|
30
|
+
"""This function runs migrations in offline mode using URL-only context."""
|
|
31
|
+
|
|
32
|
+
url = config.get_main_option("sqlalchemy.url")
|
|
33
|
+
context.configure(
|
|
34
|
+
url=url,
|
|
35
|
+
target_metadata=target_metadata,
|
|
36
|
+
literal_binds=True,
|
|
37
|
+
compare_type=True,
|
|
38
|
+
dialect_opts={"paramstyle": "named"},
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
with context.begin_transaction():
|
|
42
|
+
context.run_migrations()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def run_migrations_online() -> None:
|
|
46
|
+
"""This function runs migrations in online mode using an engine connection."""
|
|
47
|
+
|
|
48
|
+
connectable = engine_from_config(
|
|
49
|
+
config.get_section(config.config_ini_section, {}),
|
|
50
|
+
prefix="sqlalchemy.",
|
|
51
|
+
poolclass=pool.NullPool,
|
|
52
|
+
future=True,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
with connectable.connect() as connection:
|
|
56
|
+
context.configure(connection=connection, target_metadata=target_metadata, compare_type=True)
|
|
57
|
+
|
|
58
|
+
with context.begin_transaction():
|
|
59
|
+
context.run_migrations()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
if context.is_offline_mode():
|
|
63
|
+
run_migrations_offline()
|
|
64
|
+
else:
|
|
65
|
+
run_migrations_online()
|