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 +33 -0
- loremem/_http.py +227 -0
- loremem/client.py +442 -0
- loremem/exceptions.py +30 -0
- loremem/models.py +68 -0
- loremem-0.1.0.dist-info/METADATA +202 -0
- loremem-0.1.0.dist-info/RECORD +8 -0
- loremem-0.1.0.dist-info/WHEEL +4 -0
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,,
|