prme 0.15.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 (188) hide show
  1. prme/__init__.py +317 -0
  2. prme/_temporal.py +9 -0
  3. prme/api/__init__.py +20 -0
  4. prme/api/__main__.py +64 -0
  5. prme/api/app.py +106 -0
  6. prme/api/models.py +584 -0
  7. prme/api/routes.py +1389 -0
  8. prme/api/server.py +153 -0
  9. prme/cli.py +1667 -0
  10. prme/client.py +1135 -0
  11. prme/config.py +1249 -0
  12. prme/config_audit.py +318 -0
  13. prme/epistemic/__init__.py +16 -0
  14. prme/epistemic/inference.py +99 -0
  15. prme/epistemic/matrix.py +135 -0
  16. prme/epistemic/migration.py +160 -0
  17. prme/ingestion/__init__.py +45 -0
  18. prme/ingestion/claim_merge.py +122 -0
  19. prme/ingestion/entity_merge.py +158 -0
  20. prme/ingestion/entity_references.py +102 -0
  21. prme/ingestion/errors.py +123 -0
  22. prme/ingestion/extraction.py +1661 -0
  23. prme/ingestion/graph_writer.py +224 -0
  24. prme/ingestion/grounding.py +594 -0
  25. prme/ingestion/novelty.py +181 -0
  26. prme/ingestion/pipeline.py +1752 -0
  27. prme/ingestion/planning.py +225 -0
  28. prme/ingestion/resolution.py +517 -0
  29. prme/ingestion/schema.py +301 -0
  30. prme/ingestion/supersedence.py +316 -0
  31. prme/ingestion/temporal.py +35 -0
  32. prme/integrations/__init__.py +11 -0
  33. prme/integrations/_chat_history.py +131 -0
  34. prme/integrations/coding.py +316 -0
  35. prme/integrations/coding_server.py +28 -0
  36. prme/integrations/langchain.py +287 -0
  37. prme/integrations/llamaindex.py +334 -0
  38. prme/integrations/product_candidates.py +211 -0
  39. prme/integrations/typesafe.py +579 -0
  40. prme/mcp/__init__.py +22 -0
  41. prme/mcp/__main__.py +5 -0
  42. prme/mcp/server.py +1889 -0
  43. prme/model_runtime/__init__.py +5 -0
  44. prme/model_runtime/generation.py +109 -0
  45. prme/models/__init__.py +84 -0
  46. prme/models/aggregation.py +308 -0
  47. prme/models/base.py +38 -0
  48. prme/models/consolidation.py +217 -0
  49. prme/models/credit.py +97 -0
  50. prme/models/derivation.py +194 -0
  51. prme/models/direct_store.py +54 -0
  52. prme/models/edges.py +52 -0
  53. prme/models/entity_identity.py +19 -0
  54. prme/models/events.py +65 -0
  55. prme/models/extraction.py +152 -0
  56. prme/models/extraction_work.py +46 -0
  57. prme/models/learning.py +547 -0
  58. prme/models/nodes.py +109 -0
  59. prme/models/owner_reference.py +32 -0
  60. prme/models/processing.py +82 -0
  61. prme/models/profile.py +166 -0
  62. prme/models/provenance.py +38 -0
  63. prme/models/relevance.py +730 -0
  64. prme/models/speaker.py +136 -0
  65. prme/models/temporal.py +193 -0
  66. prme/models/value_bindings.py +368 -0
  67. prme/organizer/__init__.py +50 -0
  68. prme/organizer/alias_resolution.py +412 -0
  69. prme/organizer/consolidation.py +589 -0
  70. prme/organizer/contradiction.py +94 -0
  71. prme/organizer/decay.py +178 -0
  72. prme/organizer/deduplication.py +296 -0
  73. prme/organizer/jobs.py +869 -0
  74. prme/organizer/maintenance.py +323 -0
  75. prme/organizer/merge_policy.py +36 -0
  76. prme/organizer/models.py +57 -0
  77. prme/organizer/oscillation.py +174 -0
  78. prme/organizer/profiles.py +43 -0
  79. prme/organizer/summarization.py +665 -0
  80. prme/py.typed +1 -0
  81. prme/quality/__init__.py +23 -0
  82. prme/quality/feedback.py +116 -0
  83. prme/quality/metrics.py +82 -0
  84. prme/quality/tuner.py +198 -0
  85. prme/retrieval/__init__.py +127 -0
  86. prme/retrieval/abstention.py +140 -0
  87. prme/retrieval/aggregation.py +511 -0
  88. prme/retrieval/aggregation_planning.py +165 -0
  89. prme/retrieval/answerability.py +547 -0
  90. prme/retrieval/candidates.py +566 -0
  91. prme/retrieval/claim_verification.py +1077 -0
  92. prme/retrieval/config.py +787 -0
  93. prme/retrieval/context_formatter.py +1014 -0
  94. prme/retrieval/credit.py +151 -0
  95. prme/retrieval/episode_context.py +194 -0
  96. prme/retrieval/evidence_context.py +341 -0
  97. prme/retrieval/execution.py +102 -0
  98. prme/retrieval/filtering.py +175 -0
  99. prme/retrieval/full_learning.py +275 -0
  100. prme/retrieval/learning.py +265 -0
  101. prme/retrieval/models.py +767 -0
  102. prme/retrieval/packing.py +1026 -0
  103. prme/retrieval/pipeline.py +1391 -0
  104. prme/retrieval/query_analysis.py +382 -0
  105. prme/retrieval/ranking_adjustments.py +19 -0
  106. prme/retrieval/reformulation.py +214 -0
  107. prme/retrieval/reranker.py +203 -0
  108. prme/retrieval/scope.py +30 -0
  109. prme/retrieval/scoring.py +984 -0
  110. prme/retrieval/selection.py +151 -0
  111. prme/retrieval/session_context.py +259 -0
  112. prme/retrieval/snapshots.py +251 -0
  113. prme/retrieval/temporal.py +293 -0
  114. prme/retrieval/temporal_relation_models.py +99 -0
  115. prme/retrieval/temporal_relation_providers.py +554 -0
  116. prme/retrieval/temporal_relations.py +677 -0
  117. prme/retrieval/time.py +12 -0
  118. prme/retrieval/tokenization.py +15 -0
  119. prme/storage/__init__.py +25 -0
  120. prme/storage/_snapshot_json.py +117 -0
  121. prme/storage/_threading.py +41 -0
  122. prme/storage/alias_proposal.py +657 -0
  123. prme/storage/alias_review.py +616 -0
  124. prme/storage/blobs.py +90 -0
  125. prme/storage/citations.py +117 -0
  126. prme/storage/condition_evaluation.py +425 -0
  127. prme/storage/consolidation_publication.py +762 -0
  128. prme/storage/consolidation_retirement.py +163 -0
  129. prme/storage/derivation.py +232 -0
  130. prme/storage/derivation_registry.py +154 -0
  131. prme/storage/derivation_staging.py +57 -0
  132. prme/storage/duckpgq_graph.py +2020 -0
  133. prme/storage/durable_queue.py +97 -0
  134. prme/storage/embedding.py +512 -0
  135. prme/storage/encryption.py +460 -0
  136. prme/storage/engine.py +4360 -0
  137. prme/storage/event_store.py +730 -0
  138. prme/storage/extraction_work.py +388 -0
  139. prme/storage/fast_ingest.py +165 -0
  140. prme/storage/graph_store.py +573 -0
  141. prme/storage/index_eviction.py +48 -0
  142. prme/storage/lexical_index.py +873 -0
  143. prme/storage/lifecycle.py +232 -0
  144. prme/storage/metadata.py +30 -0
  145. prme/storage/namespace_identity.py +33 -0
  146. prme/storage/organizer_merge.py +216 -0
  147. prme/storage/oscillation_penalty.py +418 -0
  148. prme/storage/pack_manifest.py +118 -0
  149. prme/storage/pack_tools.py +438 -0
  150. prme/storage/pg/__init__.py +26 -0
  151. prme/storage/pg/event_store.py +600 -0
  152. prme/storage/pg/graph_store.py +1494 -0
  153. prme/storage/pg/lexical_index.py +191 -0
  154. prme/storage/pg/namespace.py +131 -0
  155. prme/storage/pg/pool.py +35 -0
  156. prme/storage/pg/schema.py +261 -0
  157. prme/storage/pg/vector_index.py +240 -0
  158. prme/storage/pg/vector_sql.py +44 -0
  159. prme/storage/pg/workspace_catalog.py +75 -0
  160. prme/storage/profile_collection.py +272 -0
  161. prme/storage/profile_publication.py +270 -0
  162. prme/storage/profile_registry.py +292 -0
  163. prme/storage/profile_retirement.py +126 -0
  164. prme/storage/profile_work.py +539 -0
  165. prme/storage/provenance.py +101 -0
  166. prme/storage/ranking_profiles.py +284 -0
  167. prme/storage/reinforcement.py +281 -0
  168. prme/storage/relevance.py +201 -0
  169. prme/storage/retention.py +369 -0
  170. prme/storage/schema.py +575 -0
  171. prme/storage/supersedence.py +261 -0
  172. prme/storage/transition_evidence.py +48 -0
  173. prme/storage/vector_index.py +872 -0
  174. prme/storage/write_queue.py +271 -0
  175. prme/types.py +301 -0
  176. prme/vision/__init__.py +44 -0
  177. prme/vision/describe.py +71 -0
  178. prme/vision/errors.py +33 -0
  179. prme/vision/frames.py +343 -0
  180. prme/vision/keyframes.py +158 -0
  181. prme/vision/observe.py +375 -0
  182. prme/vision/provider.py +98 -0
  183. prme/vision/repeat.py +44 -0
  184. prme/workspace.py +469 -0
  185. prme-0.15.0.dist-info/METADATA +329 -0
  186. prme-0.15.0.dist-info/RECORD +188 -0
  187. prme-0.15.0.dist-info/WHEEL +4 -0
  188. prme-0.15.0.dist-info/entry_points.txt +4 -0
prme/__init__.py ADDED
@@ -0,0 +1,317 @@
1
+ """PRME - Portable Relational Memory Engine.
2
+
3
+ A local-first, embeddable memory substrate for LLM-powered systems.
4
+ Combines event sourcing, graph-based relational modeling, hybrid retrieval,
5
+ and explicit or opportunistic memory reorganization.
6
+ """
7
+
8
+ __version__ = "0.15.0"
9
+
10
+ from typing import TYPE_CHECKING
11
+
12
+ if TYPE_CHECKING:
13
+ from prme.workspace import (
14
+ MemoryWorkspace,
15
+ NamespaceInfo,
16
+ NamespaceMemory,
17
+ WorkspaceError,
18
+ )
19
+ from prme.client import MemoryClient, config_from_directory, config_from_pack
20
+ from prme.ingestion.pipeline import IngestionPipeline
21
+ from prme.retrieval.models import RetrievalResponse
22
+ from prme.retrieval.pipeline import RetrievalPipeline
23
+
24
+ from prme.config import PRMEConfig
25
+ from prme.config_audit import (
26
+ HypothesisAudit as HypothesisAudit,
27
+ HypothesisSetting as HypothesisSetting,
28
+ audit_hypotheses as audit_hypotheses,
29
+ )
30
+ from prme.ingestion.errors import ExtractionError, MaterializationError
31
+ from prme.models.aggregation import (
32
+ AssertionAggregation,
33
+ AssertionGroup,
34
+ AssertionQuery,
35
+ QuantityAggregation,
36
+ QuantityAggregationPlan,
37
+ QuantityAggregationQuery,
38
+ QuantityGroup,
39
+ QuantitySample,
40
+ PlannedQuantityAggregation,
41
+ )
42
+ from prme.models.temporal import (
43
+ AssertionState,
44
+ AssertionStateConflict,
45
+ AssertionStateEntry,
46
+ AssertionStateHistoricalCoverage,
47
+ AssertionStateQuery,
48
+ AssertionStateValue,
49
+ )
50
+ from prme.models.processing import (
51
+ FastIngestItem,
52
+ ProcessingResult,
53
+ ProcessingStatus,
54
+ StoreReceipt,
55
+ )
56
+ from prme.models.value_bindings import (
57
+ MemoryValueBinding,
58
+ RetrievedValueBinding,
59
+ ToolArgumentBindingUse,
60
+ ToolArgumentReplacement,
61
+ ToolArgumentResolution,
62
+ )
63
+ from prme.storage.fast_ingest import FastIngestConflict
64
+ from prme.storage.alias_review import (
65
+ AliasProposalDecision,
66
+ AliasProposalInboxItem,
67
+ AliasProposalReviewConflict,
68
+ AliasProposalReviewRecord,
69
+ AliasProposalReviewResult,
70
+ AliasProposalStatus,
71
+ StaleAliasProposal,
72
+ )
73
+ from prme.integrations.product_candidates import (
74
+ PRODUCT_CANDIDATE_POLICY,
75
+ ProductAlignmentCandidate,
76
+ ProductCandidateEntity,
77
+ rank_product_alignment_candidates,
78
+ )
79
+ from prme.models.credit import ContextAblation, ContextPresenceCredit
80
+ from prme.models.profile import (
81
+ StaleProfileError,
82
+ ProfileJobStatus,
83
+ ProfileProcessingResult,
84
+ ProfileCollectionResult,
85
+ )
86
+ from prme.models.relevance import (
87
+ AnswerCitationRecord,
88
+ AnswerCitationSubmission,
89
+ RelevanceRecord,
90
+ RelevanceSubmission,
91
+ RetrievalReceipt,
92
+ )
93
+ from prme.models.provenance import NodeProvenance, OperationAuditRecord
94
+ from prme.models.learning import (
95
+ FullRetrievalEvaluation,
96
+ FullRetrievalEvaluationConfig,
97
+ FullRetrievalQueryResult,
98
+ FullRetrievalTrial,
99
+ LearningConfig,
100
+ LearningEvaluation,
101
+ RankingMultipliers,
102
+ RankingProfile,
103
+ RankingProfileApplication,
104
+ RankingProfileState,
105
+ RankingProfileStatus,
106
+ )
107
+ from prme.models.extraction import ExtractionRecord
108
+ from prme.models.extraction_work import ExtractionStatus, ExtractionProcessingResult
109
+ from prme.storage.engine import MemoryEngine
110
+ from prme.storage.namespace_identity import NamespaceIdentityError
111
+ from prme.storage.ranking_profiles import StaleRankingProfileError
112
+ from prme.storage.embedding import (
113
+ CachedEmbeddingProvider,
114
+ EmbeddingProvider,
115
+ QueryEmbeddingProvider,
116
+ )
117
+ from prme.retrieval.credit import ablate_context, assess_context_presence
118
+ from prme.retrieval.answerability import (
119
+ AnswerabilityAction,
120
+ AnswerabilityAssessment,
121
+ AnswerabilityConfig,
122
+ AnswerabilityError,
123
+ AnswerabilityEvaluator,
124
+ AnswerabilityRequirement,
125
+ AnswerabilityStatus,
126
+ AnswerabilityVerdict,
127
+ assess_answerability,
128
+ )
129
+ from prme.retrieval.claim_verification import (
130
+ ClaimEvidence,
131
+ ClaimVerification,
132
+ ClaimVerificationConfig,
133
+ ClaimVerificationDecisionBasis,
134
+ ClaimVerificationError,
135
+ ClaimVerificationLimitation,
136
+ ClaimVerificationStatus,
137
+ ClaimVerifier,
138
+ EvidenceGroupScore,
139
+ LocalizedEvidenceAssessment,
140
+ )
141
+ from prme.retrieval.full_learning import evaluate_full_retrieval
142
+ from prme.retrieval.temporal_relation_models import TemporalRelationMetadata
143
+ from prme.retrieval.temporal_relations import TemporalRelationConfig
144
+ from prme.types import (
145
+ ConditionEvaluationMethod,
146
+ ConditionState,
147
+ DECAY_LAMBDAS,
148
+ DEFAULT_DECAY_PROFILE_MAPPING,
149
+ DecayProfile,
150
+ EdgeType,
151
+ EpistemicType,
152
+ LifecycleState,
153
+ NodeType,
154
+ RetrievalMode,
155
+ Scope,
156
+ SourceType,
157
+ )
158
+
159
+
160
+ def __getattr__(name: str):
161
+ """Lazy imports for heavy modules to avoid circular import chains."""
162
+ if name in {
163
+ "MemoryWorkspace",
164
+ "NamespaceInfo",
165
+ "NamespaceMemory",
166
+ "WorkspaceError",
167
+ }:
168
+ from prme import workspace
169
+
170
+ return getattr(workspace, name)
171
+ if name == "MemoryClient":
172
+ from prme.client import MemoryClient
173
+
174
+ return MemoryClient
175
+ if name == "config_from_directory":
176
+ from prme.client import config_from_directory
177
+
178
+ return config_from_directory
179
+ if name == "config_from_pack":
180
+ from prme.client import config_from_pack
181
+
182
+ return config_from_pack
183
+ if name == "IngestionPipeline":
184
+ from prme.ingestion.pipeline import IngestionPipeline
185
+
186
+ return IngestionPipeline
187
+ if name == "RetrievalResponse":
188
+ from prme.retrieval.models import RetrievalResponse
189
+
190
+ return RetrievalResponse
191
+ if name == "RetrievalPipeline":
192
+ from prme.retrieval.pipeline import RetrievalPipeline
193
+
194
+ return RetrievalPipeline
195
+ raise AttributeError(f"module 'prme' has no attribute {name!r}")
196
+
197
+
198
+ __all__ = [
199
+ "AnswerCitationRecord",
200
+ "AnswerCitationSubmission",
201
+ "AliasProposalDecision",
202
+ "AliasProposalInboxItem",
203
+ "AliasProposalReviewConflict",
204
+ "AliasProposalReviewRecord",
205
+ "AliasProposalReviewResult",
206
+ "AliasProposalStatus",
207
+ "AnswerabilityAction",
208
+ "AnswerabilityAssessment",
209
+ "AnswerabilityConfig",
210
+ "AnswerabilityError",
211
+ "AnswerabilityEvaluator",
212
+ "AnswerabilityRequirement",
213
+ "AnswerabilityStatus",
214
+ "AnswerabilityVerdict",
215
+ "AssertionAggregation",
216
+ "AssertionGroup",
217
+ "AssertionQuery",
218
+ "AssertionState",
219
+ "AssertionStateConflict",
220
+ "AssertionStateEntry",
221
+ "AssertionStateHistoricalCoverage",
222
+ "AssertionStateQuery",
223
+ "AssertionStateValue",
224
+ "ClaimEvidence",
225
+ "ClaimVerification",
226
+ "ClaimVerificationConfig",
227
+ "ClaimVerificationDecisionBasis",
228
+ "ClaimVerificationError",
229
+ "ClaimVerificationLimitation",
230
+ "ClaimVerificationStatus",
231
+ "ClaimVerifier",
232
+ "LocalizedEvidenceAssessment",
233
+ "QuantityAggregation",
234
+ "QuantityAggregationPlan",
235
+ "QuantityAggregationQuery",
236
+ "QuantityGroup",
237
+ "QuantitySample",
238
+ "PlannedQuantityAggregation",
239
+ "ConditionEvaluationMethod",
240
+ "ConditionState",
241
+ "ContextAblation",
242
+ "ContextPresenceCredit",
243
+ "DECAY_LAMBDAS",
244
+ "DEFAULT_DECAY_PROFILE_MAPPING",
245
+ "DecayProfile",
246
+ "CachedEmbeddingProvider",
247
+ "EmbeddingProvider",
248
+ "QueryEmbeddingProvider",
249
+ "EdgeType",
250
+ "EpistemicType",
251
+ "EvidenceGroupScore",
252
+ "ExtractionError",
253
+ "ExtractionRecord",
254
+ "ExtractionStatus",
255
+ "ExtractionProcessingResult",
256
+ "FastIngestItem",
257
+ "FastIngestConflict",
258
+ "IngestionPipeline",
259
+ "LifecycleState",
260
+ "LearningConfig",
261
+ "LearningEvaluation",
262
+ "FullRetrievalEvaluation",
263
+ "FullRetrievalEvaluationConfig",
264
+ "FullRetrievalQueryResult",
265
+ "FullRetrievalTrial",
266
+ "MaterializationError",
267
+ "MemoryClient",
268
+ "MemoryEngine",
269
+ "MemoryValueBinding",
270
+ "MemoryWorkspace",
271
+ "NamespaceInfo",
272
+ "NamespaceMemory",
273
+ "NamespaceIdentityError",
274
+ "WorkspaceError",
275
+ "NodeType",
276
+ "NodeProvenance",
277
+ "OperationAuditRecord",
278
+ "PRMEConfig",
279
+ "ProcessingResult",
280
+ "ProcessingStatus",
281
+ "PRODUCT_CANDIDATE_POLICY",
282
+ "ProductAlignmentCandidate",
283
+ "ProductCandidateEntity",
284
+ "ProfileJobStatus",
285
+ "ProfileProcessingResult",
286
+ "ProfileCollectionResult",
287
+ "RetrievalPipeline",
288
+ "RetrievalResponse",
289
+ "RetrievalReceipt",
290
+ "RetrievedValueBinding",
291
+ "RetrievalMode",
292
+ "RankingMultipliers",
293
+ "RankingProfile",
294
+ "RankingProfileApplication",
295
+ "RankingProfileState",
296
+ "RankingProfileStatus",
297
+ "RelevanceSubmission",
298
+ "RelevanceRecord",
299
+ "Scope",
300
+ "SourceType",
301
+ "StaleProfileError",
302
+ "StaleAliasProposal",
303
+ "StaleRankingProfileError",
304
+ "StoreReceipt",
305
+ "TemporalRelationConfig",
306
+ "TemporalRelationMetadata",
307
+ "ToolArgumentBindingUse",
308
+ "ToolArgumentReplacement",
309
+ "ToolArgumentResolution",
310
+ "ablate_context",
311
+ "assess_context_presence",
312
+ "assess_answerability",
313
+ "evaluate_full_retrieval",
314
+ "rank_product_alignment_candidates",
315
+ "config_from_directory",
316
+ "config_from_pack",
317
+ ]
prme/_temporal.py ADDED
@@ -0,0 +1,9 @@
1
+ """Guard PRME's entries into dateparser's process-wide mutable parser state."""
2
+
3
+ from threading import RLock
4
+
5
+ # Older supported dateparser versions share settings and lazy locale caches.
6
+ # Ingestion and retrieval can run on different MemoryClient/event-loop threads.
7
+ # Keep every PRME parse/search call in the same critical section; an independent
8
+ # lock per pipeline does not protect either shared settings or locale caches.
9
+ DATEPARSER_LOCK = RLock()
prme/api/__init__.py ADDED
@@ -0,0 +1,20 @@
1
+ """PRME HTTP API layer (FastAPI).
2
+
3
+ Provides REST endpoints for store, retrieve, organize, and lifecycle
4
+ operations. The API is an optional dependency — install with:
5
+
6
+ pip install prme[api]
7
+
8
+ Usage:
9
+
10
+ from prme.api.app import create_app
11
+ app = create_app()
12
+
13
+ Or run directly:
14
+
15
+ python -m prme.api
16
+ """
17
+
18
+ from prme.api.app import create_app
19
+
20
+ __all__ = ["create_app"]
prme/api/__main__.py ADDED
@@ -0,0 +1,64 @@
1
+ """Entry point for ``python -m prme.api``.
2
+
3
+ Starts the PRME HTTP API server with default settings.
4
+ Configuration is loaded from environment variables (PRME_ prefix).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import argparse
10
+
11
+
12
+ def main() -> None:
13
+ """Parse CLI arguments and start the server."""
14
+ parser = argparse.ArgumentParser(
15
+ description="PRME HTTP API Server",
16
+ prog="python -m prme.api",
17
+ # The unauthenticated override must be spelled out in full.
18
+ allow_abbrev=False,
19
+ )
20
+ parser.add_argument(
21
+ "--host",
22
+ default="127.0.0.1",
23
+ help=(
24
+ "Bind address (default: 127.0.0.1). A non-loopback address such "
25
+ "as 0.0.0.0 requires API credentials (PRME_API_USER_KEYS or "
26
+ "PRME_API_API_KEY); without them startup is refused. Only literal "
27
+ "loopback addresses and localhost count as loopback."
28
+ ),
29
+ )
30
+ parser.add_argument(
31
+ "--port", type=int, default=8000, help="Port (default: 8000)"
32
+ )
33
+ parser.add_argument(
34
+ "--log-level", default="info", help="Log level (default: info)"
35
+ )
36
+ parser.add_argument(
37
+ "--allow-unauthenticated-external-bind",
38
+ action="store_true",
39
+ help=(
40
+ "Start on a non-loopback --host with no API credentials. Only for "
41
+ "a server reachable solely through an authenticating reverse "
42
+ "proxy: the API itself accepts every request, and every caller "
43
+ "has operator access to all users' memories."
44
+ ),
45
+ )
46
+ args = parser.parse_args()
47
+
48
+ from prme.api.server import UnauthenticatedBindError, run_server
49
+
50
+ try:
51
+ run_server(
52
+ host=args.host,
53
+ port=args.port,
54
+ log_level=args.log_level,
55
+ allow_unauthenticated_external_bind=(
56
+ args.allow_unauthenticated_external_bind
57
+ ),
58
+ )
59
+ except UnauthenticatedBindError as exc:
60
+ parser.error(str(exc))
61
+
62
+
63
+ if __name__ == "__main__":
64
+ main()
prme/api/app.py ADDED
@@ -0,0 +1,106 @@
1
+ """PRME FastAPI application factory.
2
+
3
+ Creates and configures the FastAPI app with lifespan management
4
+ for the MemoryEngine. The engine is stored in app.state and shared
5
+ across all request handlers.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ from contextlib import asynccontextmanager
12
+ from typing import AsyncIterator
13
+
14
+ from fastapi import FastAPI, Request
15
+ from fastapi.middleware.cors import CORSMiddleware
16
+ from fastapi.responses import JSONResponse
17
+
18
+ from prme import __version__
19
+ from prme.config import PRMEConfig
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ def create_app(config: PRMEConfig | None = None) -> FastAPI:
25
+ """Create and configure the PRME FastAPI application.
26
+
27
+ Args:
28
+ config: Optional PRME configuration. Defaults to PRMEConfig().
29
+
30
+ Returns:
31
+ Configured FastAPI application with engine lifespan management.
32
+ """
33
+ if config is None:
34
+ config = PRMEConfig()
35
+
36
+ @asynccontextmanager
37
+ async def lifespan(app: FastAPI) -> AsyncIterator[None]:
38
+ """Manage MemoryEngine lifecycle."""
39
+ from prme.storage.engine import MemoryEngine
40
+
41
+ logger.info("Starting PRME MemoryEngine...")
42
+ engine = await MemoryEngine.create(config)
43
+ app.state.engine = engine
44
+ logger.info("PRME MemoryEngine ready (backend=%s)", config.backend)
45
+
46
+ yield
47
+
48
+ logger.info("Shutting down PRME MemoryEngine...")
49
+ await engine.close()
50
+ app.state.engine = None
51
+ logger.info("PRME MemoryEngine shut down")
52
+
53
+ app = FastAPI(
54
+ title="PRME API",
55
+ description="Portable Relational Memory Engine — HTTP API",
56
+ version=__version__,
57
+ lifespan=lifespan,
58
+ )
59
+
60
+ # Expose config to request handlers (auth dependency reads it).
61
+ app.state.config = config
62
+ app.state.engine = None
63
+
64
+ # CORS — disabled unless origins are explicitly pinned (issue #34).
65
+ # Credentials are never allowed with a wildcard origin: Starlette
66
+ # reflects the request Origin in that combination, letting any
67
+ # website read responses cross-origin.
68
+ cors_origins = config.api.cors_origins
69
+ if cors_origins:
70
+ allow_credentials = (
71
+ config.api.cors_allow_credentials and "*" not in cors_origins
72
+ )
73
+ if config.api.cors_allow_credentials and not allow_credentials:
74
+ logger.warning(
75
+ "cors_allow_credentials ignored: wildcard origin in "
76
+ "cors_origins. Pin explicit origins to allow credentials."
77
+ )
78
+ app.add_middleware(
79
+ CORSMiddleware,
80
+ allow_origins=cors_origins,
81
+ allow_credentials=allow_credentials,
82
+ allow_methods=["*"],
83
+ allow_headers=["*"],
84
+ )
85
+
86
+ # Generic handler for unexpected errors: log details server-side,
87
+ # never echo internals to the caller (issue #34).
88
+ @app.exception_handler(Exception)
89
+ async def unhandled_exception_handler(
90
+ request: Request, exc: Exception
91
+ ) -> JSONResponse:
92
+ logger.error(
93
+ "Unhandled error on %s %s", request.method, request.url.path,
94
+ exc_info=exc,
95
+ )
96
+ return JSONResponse(
97
+ status_code=500, content={"detail": "Internal server error"}
98
+ )
99
+
100
+ # Register routes
101
+ from prme.api.routes import public_router, router
102
+
103
+ app.include_router(public_router)
104
+ app.include_router(router)
105
+
106
+ return app