loremem 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.
loremem/__init__.py ADDED
@@ -0,0 +1,33 @@
1
+ """
2
+ loremem — The Python SDK for Lore Organizational Memory.
3
+
4
+ Quick start:
5
+ from loremem import LoreClient
6
+
7
+ client = LoreClient(
8
+ api_key="sk-lore-xxxx",
9
+ workspace_id="ws_yourworkspace",
10
+ )
11
+
12
+ # Before your LLM call
13
+ ctx = client.get_context(query="Draft MSA for Acme Corp", tool="contract-agent")
14
+ system_prompt = ctx.formatted_injection + base_system_prompt
15
+
16
+ # After a human corrects the AI output
17
+ client.report_correction(
18
+ ai_output_id="output_001",
19
+ summary="Changed jurisdiction from UK to US",
20
+ tool="contract-agent",
21
+ )
22
+ """
23
+
24
+ from loremem.client import AsyncLoreClient, LoreClient
25
+ from loremem.models import ContextResponse, ReportResult
26
+
27
+ __version__ = "0.1.0"
28
+ __all__ = [
29
+ "LoreClient",
30
+ "AsyncLoreClient",
31
+ "ContextResponse",
32
+ "ReportResult",
33
+ ]
loremem/_http.py ADDED
@@ -0,0 +1,227 @@
1
+ """
2
+ loremem HTTP transport layer.
3
+
4
+ Handles:
5
+ - Bearer auth header injection
6
+ - Retry with exponential backoff (3 attempts)
7
+ - Timeout enforcement (5s context, 10s writes)
8
+ - Error classification into loremem exceptions
9
+ - Async variant (AsyncTransport) for async callers
10
+
11
+ httpx is used for both sync and async. It is the only required dependency.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import logging
17
+ import time
18
+ from typing import Any
19
+
20
+ import httpx
21
+
22
+ from loremem.exceptions import (
23
+ AuthError,
24
+ LoreMemError,
25
+ NetworkError,
26
+ RateLimitError,
27
+ ServerError,
28
+ )
29
+
30
+ logger = logging.getLogger("loremem")
31
+
32
+ _MAX_RETRIES = 3
33
+ _BACKOFF_BASE = 0.5 # seconds — 0.5, 1.0, 2.0
34
+ _CONTEXT_TIMEOUT = 5.0 # seconds — context must be fast
35
+ _WRITE_TIMEOUT = 10.0 # seconds — reporting calls are less time-sensitive
36
+
37
+
38
+ def _classify(response: httpx.Response) -> None:
39
+ """Raise the appropriate exception for a non-2xx response."""
40
+ status = response.status_code
41
+ try:
42
+ detail = response.json().get("detail", response.text)
43
+ except Exception:
44
+ detail = response.text or str(status)
45
+
46
+ if status == 401:
47
+ raise AuthError(f"Authentication failed: {detail}")
48
+ if status == 403:
49
+ raise AuthError(f"Forbidden — workspace mismatch: {detail}")
50
+ if status == 429:
51
+ raise RateLimitError(f"Rate limit exceeded: {detail}")
52
+ if status >= 500:
53
+ raise ServerError(f"Lore API error {status}: {detail}")
54
+ raise LoreMemError(f"Unexpected response {status}: {detail}")
55
+
56
+
57
+ def _should_retry(exc: Exception) -> bool:
58
+ """Only retry on network errors and 5xx; not on 4xx."""
59
+ return isinstance(exc, (NetworkError, ServerError, httpx.TransportError))
60
+
61
+
62
+ # ── Sync transport ────────────────────────────────────────────────────────────
63
+
64
+
65
+ class Transport:
66
+ """Synchronous HTTP transport with retry + backoff."""
67
+
68
+ def __init__(self, base_url: str, api_key: str) -> None:
69
+ self._base_url = base_url.rstrip("/")
70
+ self._headers = {
71
+ "Authorization": f"Bearer {api_key}",
72
+ "Content-Type": "application/json",
73
+ "User-Agent": "loremem-python/0.1.0",
74
+ }
75
+
76
+ def post(
77
+ self,
78
+ path: str,
79
+ payload: dict[str, Any],
80
+ timeout: float = _WRITE_TIMEOUT,
81
+ ) -> dict[str, Any]:
82
+ """POST with retry. Returns parsed JSON body."""
83
+ url = f"{self._base_url}{path}"
84
+ last_exc: Exception = LoreMemError("Unknown error")
85
+
86
+ for attempt in range(_MAX_RETRIES):
87
+ try:
88
+ response = httpx.post(
89
+ url,
90
+ json=payload,
91
+ headers=self._headers,
92
+ timeout=timeout,
93
+ )
94
+ if response.is_success:
95
+ return response.json()
96
+ _classify(response)
97
+
98
+ except (AuthError, RateLimitError, LoreMemError):
99
+ raise # non-retryable
100
+ except httpx.TimeoutException as exc:
101
+ last_exc = NetworkError(f"Request timed out: {exc}")
102
+ except httpx.TransportError as exc:
103
+ last_exc = NetworkError(f"Network error: {exc}")
104
+ except ServerError as exc:
105
+ last_exc = exc
106
+
107
+ if attempt < _MAX_RETRIES - 1:
108
+ time.sleep(_BACKOFF_BASE * (2 ** attempt))
109
+
110
+ raise last_exc
111
+
112
+ def get(
113
+ self,
114
+ path: str,
115
+ params: dict[str, Any] | None = None,
116
+ timeout: float = _WRITE_TIMEOUT,
117
+ ) -> dict[str, Any]:
118
+ """GET with retry. Returns parsed JSON body."""
119
+ url = f"{self._base_url}{path}"
120
+ last_exc: Exception = LoreMemError("Unknown error")
121
+
122
+ for attempt in range(_MAX_RETRIES):
123
+ try:
124
+ response = httpx.get(
125
+ url,
126
+ params=params,
127
+ headers=self._headers,
128
+ timeout=timeout,
129
+ )
130
+ if response.is_success:
131
+ return response.json()
132
+ _classify(response)
133
+
134
+ except (AuthError, RateLimitError, LoreMemError):
135
+ raise
136
+ except httpx.TimeoutException as exc:
137
+ last_exc = NetworkError(f"Request timed out: {exc}")
138
+ except httpx.TransportError as exc:
139
+ last_exc = NetworkError(f"Network error: {exc}")
140
+ except ServerError as exc:
141
+ last_exc = exc
142
+
143
+ if attempt < _MAX_RETRIES - 1:
144
+ time.sleep(_BACKOFF_BASE * (2 ** attempt))
145
+
146
+ raise last_exc
147
+
148
+
149
+ # ── Async transport ───────────────────────────────────────────────────────────
150
+
151
+
152
+ class AsyncTransport:
153
+ """Async HTTP transport with retry + backoff. Use with async AI agent frameworks."""
154
+
155
+ def __init__(self, base_url: str, api_key: str) -> None:
156
+ self._base_url = base_url.rstrip("/")
157
+ self._headers = {
158
+ "Authorization": f"Bearer {api_key}",
159
+ "Content-Type": "application/json",
160
+ "User-Agent": "loremem-python/0.1.0",
161
+ }
162
+
163
+ async def post(
164
+ self,
165
+ path: str,
166
+ payload: dict[str, Any],
167
+ timeout: float = _WRITE_TIMEOUT,
168
+ ) -> dict[str, Any]:
169
+ import asyncio
170
+
171
+ url = f"{self._base_url}{path}"
172
+ last_exc: Exception = LoreMemError("Unknown error")
173
+
174
+ async with httpx.AsyncClient(headers=self._headers, timeout=timeout) as client:
175
+ for attempt in range(_MAX_RETRIES):
176
+ try:
177
+ response = await client.post(url, json=payload)
178
+ if response.is_success:
179
+ return response.json()
180
+ _classify(response)
181
+
182
+ except (AuthError, RateLimitError, LoreMemError):
183
+ raise
184
+ except httpx.TimeoutException as exc:
185
+ last_exc = NetworkError(f"Request timed out: {exc}")
186
+ except httpx.TransportError as exc:
187
+ last_exc = NetworkError(f"Network error: {exc}")
188
+ except ServerError as exc:
189
+ last_exc = exc
190
+
191
+ if attempt < _MAX_RETRIES - 1:
192
+ await asyncio.sleep(_BACKOFF_BASE * (2 ** attempt))
193
+
194
+ raise last_exc
195
+
196
+ async def get(
197
+ self,
198
+ path: str,
199
+ params: dict[str, Any] | None = None,
200
+ timeout: float = _WRITE_TIMEOUT,
201
+ ) -> dict[str, Any]:
202
+ import asyncio
203
+
204
+ url = f"{self._base_url}{path}"
205
+ last_exc: Exception = LoreMemError("Unknown error")
206
+
207
+ async with httpx.AsyncClient(headers=self._headers, timeout=timeout) as client:
208
+ for attempt in range(_MAX_RETRIES):
209
+ try:
210
+ response = await client.get(url, params=params)
211
+ if response.is_success:
212
+ return response.json()
213
+ _classify(response)
214
+
215
+ except (AuthError, RateLimitError, LoreMemError):
216
+ raise
217
+ except httpx.TimeoutException as exc:
218
+ last_exc = NetworkError(f"Request timed out: {exc}")
219
+ except httpx.TransportError as exc:
220
+ last_exc = NetworkError(f"Network error: {exc}")
221
+ except ServerError as exc:
222
+ last_exc = exc
223
+
224
+ if attempt < _MAX_RETRIES - 1:
225
+ await asyncio.sleep(_BACKOFF_BASE * (2 ** attempt))
226
+
227
+ raise last_exc
loremem/client.py ADDED
@@ -0,0 +1,442 @@
1
+ """
2
+ LoreClient — sync and async clients for the Lore context injection API.
3
+
4
+ Sync usage (most AI agents):
5
+ from loremem import LoreClient
6
+
7
+ client = LoreClient(
8
+ api_key="sk-lore-xxxx",
9
+ workspace_id="ws_yourworkspace",
10
+ )
11
+
12
+ # Before every LLM call
13
+ context = client.get_context(
14
+ query="Draft an MSA for Acme Corp",
15
+ tool="contract-drafting-agent",
16
+ )
17
+ # context.formatted_injection → prepend to system prompt
18
+
19
+ Async usage (async agent frameworks — LangChain, CrewAI, etc.):
20
+ from loremem import AsyncLoreClient
21
+
22
+ client = AsyncLoreClient(api_key="sk-lore-xxxx", workspace_id="ws_acme")
23
+ context = await client.get_context(query="...", tool="...")
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import logging
29
+ from typing import Any
30
+
31
+ from loremem._http import AsyncTransport, Transport, _CONTEXT_TIMEOUT
32
+ from loremem.exceptions import LoreMemError
33
+ from loremem.models import ContextResponse, ReportResult
34
+
35
+ logger = logging.getLogger("loremem")
36
+
37
+ _DEFAULT_BASE_URL = "https://lore-m0st.onrender.com"
38
+
39
+
40
+ # ── Sync client ───────────────────────────────────────────────────────────────
41
+
42
+
43
+ class LoreClient:
44
+ """
45
+ Synchronous Lore client.
46
+
47
+ Parameters
48
+ ----------
49
+ api_key:
50
+ Your Lore API key (starts with ``sk-lore-``). Get one from
51
+ ``POST /v1/auth/api-keys`` or the Lore dashboard.
52
+ workspace_id:
53
+ The workspace this client operates within.
54
+ base_url:
55
+ Override the API base URL (default: production Render service).
56
+ Set to ``http://localhost:8000`` during local development.
57
+ """
58
+
59
+ def __init__(
60
+ self,
61
+ api_key: str,
62
+ workspace_id: str,
63
+ base_url: str = _DEFAULT_BASE_URL,
64
+ ) -> None:
65
+ if not api_key:
66
+ raise ValueError("api_key must not be empty.")
67
+ if not workspace_id:
68
+ raise ValueError("workspace_id must not be empty.")
69
+
70
+ self._workspace_id = workspace_id
71
+ self._http = Transport(base_url=base_url, api_key=api_key)
72
+
73
+ # ── Public API ────────────────────────────────────────────────────────────
74
+
75
+ def get_context(
76
+ self,
77
+ query: str,
78
+ tool: str,
79
+ hints: dict[str, Any] | None = None,
80
+ entities: list[str] | None = None,
81
+ max_rules: int = 10,
82
+ max_tokens: int = 2000,
83
+ ) -> ContextResponse:
84
+ """
85
+ Retrieve relevant organizational context for an AI task.
86
+
87
+ Call this **before** your LLM call and prepend
88
+ ``response.formatted_injection`` to your system prompt.
89
+
90
+ This method **never raises**. On any error it returns an empty
91
+ ``ContextResponse`` and logs a warning so your agent continues normally.
92
+
93
+ Parameters
94
+ ----------
95
+ query:
96
+ Natural language description of the task the agent is about to perform.
97
+ E.g. ``"Draft an MSA for Acme Corp"``.
98
+ tool:
99
+ Identifier for the AI tool making the request. Use a consistent slug
100
+ per agent — e.g. ``"contract-drafting-agent"``. Rules are scoped to tool.
101
+ hints:
102
+ Optional dict of additional context tags passed to the graph query.
103
+ E.g. ``{"jurisdiction": "US", "customer_tier": "enterprise"}``.
104
+ entities:
105
+ Optional list of entity names to fetch specific profiles for.
106
+ E.g. ``["Acme Corp"]``.
107
+ max_rules:
108
+ Maximum number of rules to include in the injection block.
109
+ max_tokens:
110
+ Approximate token budget for the formatted injection string.
111
+
112
+ Returns
113
+ -------
114
+ ContextResponse
115
+ Always returns a valid object. ``formatted_injection`` is an empty
116
+ string when no context was found or on error.
117
+
118
+ Example
119
+ -------
120
+ ::
121
+
122
+ ctx = client.get_context(
123
+ query="Draft an MSA for Acme Corp",
124
+ tool="contract-agent",
125
+ hints={"jurisdiction": "US"},
126
+ entities=["Acme Corp"],
127
+ )
128
+ system_prompt = ctx.formatted_injection + "\\n\\n" + base_system_prompt
129
+ """
130
+ try:
131
+ payload: dict[str, Any] = {
132
+ "tool": tool,
133
+ "task": query,
134
+ "context_tags": hints or {},
135
+ "entities": entities or [],
136
+ "max_rules": max_rules,
137
+ "max_tokens": max_tokens,
138
+ }
139
+ data = self._http.post(
140
+ f"/v1/context?workspace_id={self._workspace_id}",
141
+ payload=payload,
142
+ timeout=_CONTEXT_TIMEOUT,
143
+ )
144
+ return ContextResponse(
145
+ context_id=data.get("context_id", ""),
146
+ formatted_injection=data.get("formatted_injection", ""),
147
+ rules=data.get("rules", []),
148
+ entities=data.get("entities", []),
149
+ decisions=data.get("decisions", []),
150
+ cached=data.get("cached", False),
151
+ )
152
+ except LoreMemError as exc:
153
+ logger.warning("loremem.get_context failed — returning empty context: %s", exc)
154
+ return ContextResponse.empty()
155
+ except Exception as exc:
156
+ logger.warning("loremem.get_context unexpected error — returning empty context: %s", exc)
157
+ return ContextResponse.empty()
158
+
159
+ def report_correction(
160
+ self,
161
+ ai_output_id: str,
162
+ summary: str,
163
+ tool: str,
164
+ context_tags: dict[str, Any] | None = None,
165
+ actor_id: str | None = None,
166
+ ) -> ReportResult:
167
+ """
168
+ Report a human correction of an AI output.
169
+
170
+ Call this whenever a human edits, overrides, or rejects an AI-generated
171
+ output. Lore learns from these corrections over time.
172
+
173
+ This method **never raises**. Errors are logged as warnings.
174
+
175
+ Parameters
176
+ ----------
177
+ ai_output_id:
178
+ ID you assigned to the AI output that was corrected. Used for
179
+ deduplication and linking back to the original output.
180
+ summary:
181
+ Human-readable description of what changed.
182
+ E.g. ``"Changed jurisdiction clause from UK to US standard"``.
183
+ tool:
184
+ The tool that generated the original output. Same slug as in
185
+ ``get_context()``.
186
+ context_tags:
187
+ Optional additional metadata about the correction context.
188
+ E.g. ``{"customer": "Acme Corp", "document_type": "MSA"}``.
189
+ actor_id:
190
+ ID of the human who made the correction (email or user ID).
191
+ Helps pattern mining distinguish corrections by different people.
192
+
193
+ Returns
194
+ -------
195
+ ReportResult
196
+ Always returns. ``accepted=True`` means the event was queued.
197
+
198
+ Example
199
+ -------
200
+ ::
201
+
202
+ client.report_correction(
203
+ ai_output_id="draft_acme_msa_v1",
204
+ summary="Changed indemnity clause to US_STANDARD template",
205
+ tool="contract-agent",
206
+ context_tags={"customer": "Acme Corp"},
207
+ actor_id="james@company.com",
208
+ )
209
+ """
210
+ try:
211
+ payload: dict[str, Any] = {
212
+ "workspace_id": self._workspace_id,
213
+ "tool": tool,
214
+ "event_type": "correction",
215
+ "ai_output_id": ai_output_id,
216
+ "actor_id": actor_id or "sdk_reporter",
217
+ "context_tags": context_tags or {},
218
+ "delta": [
219
+ {
220
+ "field": "output",
221
+ "change_type": "correction",
222
+ "change_summary": summary,
223
+ }
224
+ ],
225
+ "confidence_signal": 0.9,
226
+ }
227
+ data = self._http.post(
228
+ f"/v1/events?workspace_id={self._workspace_id}",
229
+ payload=payload,
230
+ )
231
+ return ReportResult(
232
+ accepted=True,
233
+ event_id=data.get("event_id", ""),
234
+ )
235
+ except LoreMemError as exc:
236
+ logger.warning("loremem.report_correction failed (event dropped): %s", exc)
237
+ return ReportResult(accepted=False)
238
+ except Exception as exc:
239
+ logger.warning("loremem.report_correction unexpected error (event dropped): %s", exc)
240
+ return ReportResult(accepted=False)
241
+
242
+ def report_output(
243
+ self,
244
+ output_id: str,
245
+ tool: str,
246
+ summary: str = "",
247
+ context_tags: dict[str, Any] | None = None,
248
+ actor_id: str | None = None,
249
+ ) -> ReportResult:
250
+ """
251
+ Report an AI output that was approved without modification (positive signal).
252
+
253
+ Use this when a human reviews an AI output and accepts it without changes.
254
+ This reinforces the rules that contributed to the good output.
255
+
256
+ This method **never raises**.
257
+
258
+ Parameters
259
+ ----------
260
+ output_id:
261
+ Unique ID for this AI output — the same ID you'd pass to
262
+ ``report_correction()`` if the output were later corrected.
263
+ tool:
264
+ The tool that generated this output.
265
+ summary:
266
+ Optional description of what was generated. E.g. ``"MSA draft approved"``.
267
+ context_tags:
268
+ Optional metadata about the output context.
269
+ actor_id:
270
+ ID of the human who approved the output.
271
+
272
+ Returns
273
+ -------
274
+ ReportResult
275
+ """
276
+ try:
277
+ payload: dict[str, Any] = {
278
+ "workspace_id": self._workspace_id,
279
+ "tool": tool,
280
+ "event_type": "approval",
281
+ "ai_output_id": output_id,
282
+ "actor_id": actor_id or "sdk_reporter",
283
+ "context_tags": context_tags or {},
284
+ "delta": [
285
+ {
286
+ "field": "output",
287
+ "change_type": "approval",
288
+ "change_summary": summary or "Output approved without modification",
289
+ }
290
+ ],
291
+ "confidence_signal": 1.0,
292
+ }
293
+ data = self._http.post(
294
+ f"/v1/events?workspace_id={self._workspace_id}",
295
+ payload=payload,
296
+ )
297
+ return ReportResult(
298
+ accepted=True,
299
+ event_id=data.get("event_id", ""),
300
+ )
301
+ except LoreMemError as exc:
302
+ logger.warning("loremem.report_output failed (event dropped): %s", exc)
303
+ return ReportResult(accepted=False)
304
+ except Exception as exc:
305
+ logger.warning("loremem.report_output unexpected error (event dropped): %s", exc)
306
+ return ReportResult(accepted=False)
307
+
308
+
309
+ # ── Async client ──────────────────────────────────────────────────────────────
310
+
311
+
312
+ class AsyncLoreClient:
313
+ """
314
+ Async variant of LoreClient. Same interface, all methods are coroutines.
315
+
316
+ Use with async agent frameworks (LangChain, CrewAI, FastAPI-based agents, etc.).
317
+
318
+ ::
319
+
320
+ client = AsyncLoreClient(api_key="sk-lore-xxx", workspace_id="ws_acme")
321
+ ctx = await client.get_context(query="...", tool="...")
322
+ """
323
+
324
+ def __init__(
325
+ self,
326
+ api_key: str,
327
+ workspace_id: str,
328
+ base_url: str = _DEFAULT_BASE_URL,
329
+ ) -> None:
330
+ if not api_key:
331
+ raise ValueError("api_key must not be empty.")
332
+ if not workspace_id:
333
+ raise ValueError("workspace_id must not be empty.")
334
+
335
+ self._workspace_id = workspace_id
336
+ self._http = AsyncTransport(base_url=base_url, api_key=api_key)
337
+
338
+ async def get_context(
339
+ self,
340
+ query: str,
341
+ tool: str,
342
+ hints: dict[str, Any] | None = None,
343
+ entities: list[str] | None = None,
344
+ max_rules: int = 10,
345
+ max_tokens: int = 2000,
346
+ ) -> ContextResponse:
347
+ """Async version of LoreClient.get_context(). Never raises."""
348
+ try:
349
+ payload: dict[str, Any] = {
350
+ "tool": tool,
351
+ "task": query,
352
+ "context_tags": hints or {},
353
+ "entities": entities or [],
354
+ "max_rules": max_rules,
355
+ "max_tokens": max_tokens,
356
+ }
357
+ data = await self._http.post(
358
+ f"/v1/context?workspace_id={self._workspace_id}",
359
+ payload=payload,
360
+ timeout=_CONTEXT_TIMEOUT,
361
+ )
362
+ return ContextResponse(
363
+ context_id=data.get("context_id", ""),
364
+ formatted_injection=data.get("formatted_injection", ""),
365
+ rules=data.get("rules", []),
366
+ entities=data.get("entities", []),
367
+ decisions=data.get("decisions", []),
368
+ cached=data.get("cached", False),
369
+ )
370
+ except Exception as exc:
371
+ logger.warning("loremem.get_context failed — returning empty context: %s", exc)
372
+ return ContextResponse.empty()
373
+
374
+ async def report_correction(
375
+ self,
376
+ ai_output_id: str,
377
+ summary: str,
378
+ tool: str,
379
+ context_tags: dict[str, Any] | None = None,
380
+ actor_id: str | None = None,
381
+ ) -> ReportResult:
382
+ """Async version of LoreClient.report_correction(). Never raises."""
383
+ try:
384
+ payload: dict[str, Any] = {
385
+ "workspace_id": self._workspace_id,
386
+ "tool": tool,
387
+ "event_type": "correction",
388
+ "ai_output_id": ai_output_id,
389
+ "actor_id": actor_id or "sdk_reporter",
390
+ "context_tags": context_tags or {},
391
+ "delta": [
392
+ {
393
+ "field": "output",
394
+ "change_type": "correction",
395
+ "change_summary": summary,
396
+ }
397
+ ],
398
+ "confidence_signal": 0.9,
399
+ }
400
+ data = await self._http.post(
401
+ f"/v1/events?workspace_id={self._workspace_id}",
402
+ payload=payload,
403
+ )
404
+ return ReportResult(accepted=True, event_id=data.get("event_id", ""))
405
+ except Exception as exc:
406
+ logger.warning("loremem.report_correction failed (event dropped): %s", exc)
407
+ return ReportResult(accepted=False)
408
+
409
+ async def report_output(
410
+ self,
411
+ output_id: str,
412
+ tool: str,
413
+ summary: str = "",
414
+ context_tags: dict[str, Any] | None = None,
415
+ actor_id: str | None = None,
416
+ ) -> ReportResult:
417
+ """Async version of LoreClient.report_output(). Never raises."""
418
+ try:
419
+ payload: dict[str, Any] = {
420
+ "workspace_id": self._workspace_id,
421
+ "tool": tool,
422
+ "event_type": "approval",
423
+ "ai_output_id": output_id,
424
+ "actor_id": actor_id or "sdk_reporter",
425
+ "context_tags": context_tags or {},
426
+ "delta": [
427
+ {
428
+ "field": "output",
429
+ "change_type": "approval",
430
+ "change_summary": summary or "Output approved without modification",
431
+ }
432
+ ],
433
+ "confidence_signal": 1.0,
434
+ }
435
+ data = await self._http.post(
436
+ f"/v1/events?workspace_id={self._workspace_id}",
437
+ payload=payload,
438
+ )
439
+ return ReportResult(accepted=True, event_id=data.get("event_id", ""))
440
+ except Exception as exc:
441
+ logger.warning("loremem.report_output failed (event dropped): %s", exc)
442
+ return ReportResult(accepted=False)
loremem/exceptions.py ADDED
@@ -0,0 +1,30 @@
1
+ """
2
+ loremem exceptions.
3
+
4
+ These are INTERNAL — never surfaced to SDK callers.
5
+ All public methods catch every exception, log a warning, and return a safe default.
6
+ """
7
+
8
+
9
+ class LoreMemError(Exception):
10
+ """Base exception for all loremem SDK errors."""
11
+
12
+
13
+ class AuthError(LoreMemError):
14
+ """API key rejected or workspace not found."""
15
+
16
+
17
+ class NetworkError(LoreMemError):
18
+ """Could not reach the Lore API."""
19
+
20
+
21
+ class TimeoutError(LoreMemError):
22
+ """Request timed out."""
23
+
24
+
25
+ class RateLimitError(LoreMemError):
26
+ """Workspace rate limit exceeded."""
27
+
28
+
29
+ class ServerError(LoreMemError):
30
+ """Lore API returned a 5xx response."""
loremem/models.py ADDED
@@ -0,0 +1,68 @@
1
+ """
2
+ loremem data models.
3
+
4
+ Lightweight dataclasses — no Pydantic dependency in the SDK.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass, field
10
+ from typing import Any
11
+
12
+
13
+ @dataclass
14
+ class ContextResponse:
15
+ """
16
+ Returned by LoreClient.get_context().
17
+
18
+ In practice you mostly use `formatted_injection` — prepend it to your LLM
19
+ system prompt. The other fields are available for logging / debugging.
20
+ """
21
+
22
+ context_id: str
23
+ """Unique ID for this context response — include in your audit trail."""
24
+
25
+ formatted_injection: str
26
+ """
27
+ Ready-to-use string to prepend to your LLM system prompt.
28
+ Empty string when no relevant context exists or if the request failed.
29
+ """
30
+
31
+ rules: list[dict[str, Any]] = field(default_factory=list)
32
+ """Active rules that matched this request."""
33
+
34
+ entities: list[dict[str, Any]] = field(default_factory=list)
35
+ """Entity profiles that matched this request."""
36
+
37
+ decisions: list[dict[str, Any]] = field(default_factory=list)
38
+ """Decision records that matched this request."""
39
+
40
+ cached: bool = False
41
+ """True if this response was served from cache (no graph query was run)."""
42
+
43
+ @classmethod
44
+ def empty(cls) -> "ContextResponse":
45
+ """Safe empty response — returned on any error."""
46
+ return cls(
47
+ context_id="",
48
+ formatted_injection="",
49
+ rules=[],
50
+ entities=[],
51
+ decisions=[],
52
+ cached=False,
53
+ )
54
+
55
+ def __bool__(self) -> bool:
56
+ """True if the response contains any usable context."""
57
+ return bool(self.formatted_injection)
58
+
59
+
60
+ @dataclass
61
+ class ReportResult:
62
+ """Returned by report_correction / report_output. Always succeeds (errors swallowed)."""
63
+
64
+ accepted: bool
65
+ """True if the API accepted the report."""
66
+
67
+ event_id: str = ""
68
+ """Event ID assigned by Lore, if accepted."""
@@ -0,0 +1,202 @@
1
+ Metadata-Version: 2.4
2
+ Name: loremem
3
+ Version: 0.1.0
4
+ Summary: Lore SDK — inject organizational memory into any AI agent
5
+ Project-URL: Homepage, https://github.com/mr-shakib/lore
6
+ Project-URL: Documentation, https://github.com/mr-shakib/lore/tree/main/sdk/python
7
+ Project-URL: Repository, https://github.com/mr-shakib/lore
8
+ Project-URL: Issues, https://github.com/mr-shakib/lore/issues
9
+ License: MIT
10
+ Keywords: agents,ai,context,llm,lore,memory
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Requires-Python: >=3.9
21
+ Requires-Dist: httpx>=0.27.0
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
24
+ Requires-Dist: pytest>=8.0; extra == 'dev'
25
+ Requires-Dist: respx>=0.21; extra == 'dev'
26
+ Description-Content-Type: text/markdown
27
+
28
+ # loremem — Python SDK for Lore
29
+
30
+ > Inject your company's organizational memory into any AI agent in 3 lines of code.
31
+
32
+ **Lore** captures every human correction of an AI output, structures it into a company knowledge graph, and feeds it back to your AI agents so they stop making the same mistakes twice.
33
+
34
+ `loremem` is the Python SDK for accessing Lore's Context Injection API.
35
+
36
+ ---
37
+
38
+ ## Installation
39
+
40
+ ```bash
41
+ # From PyPI (when published)
42
+ pip install loremem
43
+
44
+ # From GitHub (MVP — no PyPI publish required)
45
+ pip install git+https://github.com/mr-shakib/lore#subdirectory=sdk/python
46
+ ```
47
+
48
+ ---
49
+
50
+ ## Quickstart (3 minutes)
51
+
52
+ ```python
53
+ from loremem import LoreClient
54
+
55
+ client = LoreClient(
56
+ api_key="sk-lore-xxxx", # from POST /v1/auth/api-keys
57
+ workspace_id="ws_yourworkspace", # your Lore workspace ID
58
+ )
59
+
60
+ # ── Step 1: Get context before your LLM call ──────────────────────────────────
61
+
62
+ ctx = client.get_context(
63
+ query="Draft an MSA for Acme Corp",
64
+ tool="contract-drafting-agent",
65
+ hints={"jurisdiction": "US", "customer_tier": "enterprise"},
66
+ entities=["Acme Corp"],
67
+ )
68
+
69
+ # Prepend to your system prompt
70
+ system_prompt = ctx.formatted_injection + "\n\n" + YOUR_BASE_SYSTEM_PROMPT
71
+ response = openai_client.chat.completions.create(
72
+ model="gpt-4o",
73
+ messages=[{"role": "system", "content": system_prompt}, ...],
74
+ )
75
+
76
+ # ── Step 2: Report corrections so Lore learns ─────────────────────────────────
77
+
78
+ # When a human edits the AI output:
79
+ client.report_correction(
80
+ ai_output_id="draft_acme_msa_v1",
81
+ summary="Changed indemnity clause from UK to US_STANDARD template",
82
+ tool="contract-drafting-agent",
83
+ context_tags={"customer": "Acme Corp", "document_type": "MSA"},
84
+ actor_id="james@company.com",
85
+ )
86
+
87
+ # When a human approves the AI output without changes (positive signal):
88
+ client.report_output(
89
+ output_id="draft_acme_msa_v2",
90
+ tool="contract-drafting-agent",
91
+ summary="MSA draft approved — no changes needed",
92
+ actor_id="james@company.com",
93
+ )
94
+ ```
95
+
96
+ After a few corrections, Lore automatically proposes rules like:
97
+ > *"US clients require the US_STANDARD indemnity template"*
98
+
99
+ Confirm the rule once → it's injected into every future AI call automatically.
100
+
101
+ ---
102
+
103
+ ## Async usage
104
+
105
+ For async agent frameworks (LangChain, CrewAI, FastAPI-based agents):
106
+
107
+ ```python
108
+ from loremem import AsyncLoreClient
109
+
110
+ client = AsyncLoreClient(api_key="sk-lore-xxxx", workspace_id="ws_acme")
111
+
112
+ ctx = await client.get_context(
113
+ query="Route this support ticket",
114
+ tool="support-triage-agent",
115
+ )
116
+
117
+ await client.report_correction(
118
+ ai_output_id="ticket_001",
119
+ summary="Re-routed from Tier 1 to Enterprise team",
120
+ tool="support-triage-agent",
121
+ )
122
+ ```
123
+
124
+ ---
125
+
126
+ ## Never-throw guarantee
127
+
128
+ Every method in `LoreClient` and `AsyncLoreClient` is designed to **never raise exceptions**. If Lore is unavailable, misconfigured, or rate-limited:
129
+
130
+ - `get_context()` returns an empty `ContextResponse` (`.formatted_injection == ""`)
131
+ - `report_correction()` and `report_output()` return `ReportResult(accepted=False)`
132
+ - A `WARNING` is logged via Python's standard `logging` module
133
+
134
+ **Lore's unavailability will never cause your AI agent to break.**
135
+
136
+ ```python
137
+ import logging
138
+ logging.getLogger("loremem").setLevel(logging.WARNING) # optional: see SDK warnings
139
+ ```
140
+
141
+ ---
142
+
143
+ ## API reference
144
+
145
+ ### `LoreClient(api_key, workspace_id, base_url?)`
146
+
147
+ | Parameter | Type | Description |
148
+ |---|---|---|
149
+ | `api_key` | `str` | Lore API key (`sk-lore-...`) |
150
+ | `workspace_id` | `str` | Your workspace ID |
151
+ | `base_url` | `str` | Default: production Lore API. Set to `http://localhost:8000` for local dev |
152
+
153
+ ### `get_context(query, tool, hints?, entities?, max_rules?, max_tokens?)`
154
+
155
+ Returns a `ContextResponse`:
156
+
157
+ | Field | Type | Description |
158
+ |---|---|---|
159
+ | `formatted_injection` | `str` | Ready-to-use string — prepend to system prompt |
160
+ | `context_id` | `str` | Unique ID for this context response |
161
+ | `rules` | `list[dict]` | Active rules that matched |
162
+ | `entities` | `list[dict]` | Entity profiles that matched |
163
+ | `decisions` | `list[dict]` | Decision records that matched |
164
+ | `cached` | `bool` | True if served from 15-min cache |
165
+
166
+ ### `report_correction(ai_output_id, summary, tool, context_tags?, actor_id?)`
167
+
168
+ Call when a human edits or overrides an AI output.
169
+
170
+ ### `report_output(output_id, tool, summary?, context_tags?, actor_id?)`
171
+
172
+ Call when a human approves an AI output unchanged (positive signal).
173
+
174
+ ---
175
+
176
+ ## Getting an API key
177
+
178
+ ```bash
179
+ # Create a key (requires Clerk JWT from the dashboard, or bootstrap via Supabase directly)
180
+ curl -X POST https://lore-m0st.onrender.com/v1/auth/api-keys \
181
+ -H "Authorization: Bearer <clerk_jwt>" \
182
+ -H "Content-Type: application/json" \
183
+ -d '{"name": "Production SDK key"}'
184
+ ```
185
+
186
+ ---
187
+
188
+ ## Local development
189
+
190
+ ```python
191
+ client = LoreClient(
192
+ api_key="sk-lore-xxxx",
193
+ workspace_id="ws_test",
194
+ base_url="http://localhost:8000", # local FastAPI server
195
+ )
196
+ ```
197
+
198
+ ---
199
+
200
+ ## License
201
+
202
+ MIT
@@ -0,0 +1,8 @@
1
+ loremem/__init__.py,sha256=ieBpPGsoEQTz4FmRpNIWU2Kh7iGcu00xLT5KHuiBV94,866
2
+ loremem/_http.py,sha256=79P1hsUw7OT9_c-HR_oez4ifiLHbR09Y5UfinolZuFs,7952
3
+ loremem/client.py,sha256=EacsYArYaY2d_LJwhuXA3pxZiAzKJte6zwThDIKirHs,16537
4
+ loremem/exceptions.py,sha256=FPtco-CbeCpMaNveWOH55yjpo2p5VZqlpvfO6V7B6Yc,666
5
+ loremem/models.py,sha256=JPVzYn-alj6Gg7KV_vGn1vYrBTBog3WeiWxHtwmKY-M,1976
6
+ loremem-0.1.0.dist-info/METADATA,sha256=7zk8P2OAa5rVoju0ONAZKNKqxFbmOLjVrKug10N7vm4,6234
7
+ loremem-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
8
+ loremem-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any