hebbrix 2.6.2__tar.gz → 2.7.0__tar.gz
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.
- {hebbrix-2.6.2 → hebbrix-2.7.0}/CHANGELOG.md +15 -1
- {hebbrix-2.6.2/hebbrix.egg-info → hebbrix-2.7.0}/PKG-INFO +17 -4
- {hebbrix-2.6.2 → hebbrix-2.7.0}/README.md +16 -3
- {hebbrix-2.6.2 → hebbrix-2.7.0}/hebbrix/__init__.py +1 -1
- {hebbrix-2.6.2 → hebbrix-2.7.0}/hebbrix/_advice.py +15 -1
- {hebbrix-2.6.2 → hebbrix-2.7.0}/hebbrix/chat.py +46 -0
- {hebbrix-2.6.2 → hebbrix-2.7.0}/hebbrix/client.py +7 -1
- {hebbrix-2.6.2 → hebbrix-2.7.0}/hebbrix/models.py +6 -0
- {hebbrix-2.6.2 → hebbrix-2.7.0}/hebbrix/resources.py +163 -33
- {hebbrix-2.6.2 → hebbrix-2.7.0}/hebbrix/sync_client.py +56 -34
- {hebbrix-2.6.2 → hebbrix-2.7.0/hebbrix.egg-info}/PKG-INFO +17 -4
- {hebbrix-2.6.2 → hebbrix-2.7.0}/hebbrix.egg-info/SOURCES.txt +2 -0
- {hebbrix-2.6.2 → hebbrix-2.7.0}/pyproject.toml +1 -1
- {hebbrix-2.6.2 → hebbrix-2.7.0}/tests/test_advisor_integrity.py +30 -0
- hebbrix-2.7.0/tests/test_chat_answer_options.py +57 -0
- hebbrix-2.7.0/tests/test_compact_search_default.py +117 -0
- {hebbrix-2.6.2 → hebbrix-2.7.0}/tests/test_customer_developer_experience.py +1 -1
- {hebbrix-2.6.2 → hebbrix-2.7.0}/LICENSE +0 -0
- {hebbrix-2.6.2 → hebbrix-2.7.0}/MANIFEST.in +0 -0
- {hebbrix-2.6.2 → hebbrix-2.7.0}/contracts/openapi-routes.json +0 -0
- {hebbrix-2.6.2 → hebbrix-2.7.0}/docs/native-experience-operations.md +0 -0
- {hebbrix-2.6.2 → hebbrix-2.7.0}/examples/native_reflection_worker.py +0 -0
- {hebbrix-2.6.2 → hebbrix-2.7.0}/hebbrix/exceptions.py +0 -0
- {hebbrix-2.6.2 → hebbrix-2.7.0}/hebbrix/execution.py +0 -0
- {hebbrix-2.6.2 → hebbrix-2.7.0}/hebbrix/experience.py +0 -0
- {hebbrix-2.6.2 → hebbrix-2.7.0}/hebbrix/openai_reflection.py +0 -0
- {hebbrix-2.6.2 → hebbrix-2.7.0}/hebbrix/reflection_worker.py +0 -0
- {hebbrix-2.6.2 → hebbrix-2.7.0}/hebbrix/workflow.py +0 -0
- {hebbrix-2.6.2 → hebbrix-2.7.0}/hebbrix.egg-info/dependency_links.txt +0 -0
- {hebbrix-2.6.2 → hebbrix-2.7.0}/hebbrix.egg-info/entry_points.txt +0 -0
- {hebbrix-2.6.2 → hebbrix-2.7.0}/hebbrix.egg-info/requires.txt +0 -0
- {hebbrix-2.6.2 → hebbrix-2.7.0}/hebbrix.egg-info/top_level.txt +0 -0
- {hebbrix-2.6.2 → hebbrix-2.7.0}/setup.cfg +0 -0
- {hebbrix-2.6.2 → hebbrix-2.7.0}/tests/test_advanced_resources.py +0 -0
- {hebbrix-2.6.2 → hebbrix-2.7.0}/tests/test_evidence_loop_resource.py +0 -0
- {hebbrix-2.6.2 → hebbrix-2.7.0}/tests/test_evidence_safety_boundary.py +0 -0
- {hebbrix-2.6.2 → hebbrix-2.7.0}/tests/test_execution_permission.py +0 -0
- {hebbrix-2.6.2 → hebbrix-2.7.0}/tests/test_experience_request.py +0 -0
- {hebbrix-2.6.2 → hebbrix-2.7.0}/tests/test_experience_workflow.py +0 -0
- {hebbrix-2.6.2 → hebbrix-2.7.0}/tests/test_followup_helpers.py +0 -0
- {hebbrix-2.6.2 → hebbrix-2.7.0}/tests/test_memories_resource.py +0 -0
- {hebbrix-2.6.2 → hebbrix-2.7.0}/tests/test_openai_reflection.py +0 -0
- {hebbrix-2.6.2 → hebbrix-2.7.0}/tests/test_openapi_parity.py +0 -0
- {hebbrix-2.6.2 → hebbrix-2.7.0}/tests/test_outcome_advice_resource.py +0 -0
- {hebbrix-2.6.2 → hebbrix-2.7.0}/tests/test_procedural_resource.py +0 -0
- {hebbrix-2.6.2 → hebbrix-2.7.0}/tests/test_reflection_worker_cli.py +0 -0
- {hebbrix-2.6.2 → hebbrix-2.7.0}/tests/test_release_version.py +0 -0
- {hebbrix-2.6.2 → hebbrix-2.7.0}/tests/test_sync_client.py +0 -0
- {hebbrix-2.6.2 → hebbrix-2.7.0}/tests/test_workflow_route_contract.py +0 -0
|
@@ -1,12 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.7.0 — 2026-10-19 (compact responses, entity scope, chat answers)
|
|
4
|
+
|
|
5
|
+
- Behaviour change: `search()` and `search_with_proof()` now request `view="compact"` by default (rows keep content, identity, score, rank, timestamps, `low_confidence` and entity scope; the safety envelope and proof context are unchanged; ranking diagnostics need `debug=True`). Pass `view="full"` for the previous per-row metadata. `search()` returns rows only and no longer requests the unused ProofLoop token unless `include_proof_context=True`.
|
|
6
|
+
- `reason()` now requests `view="compact"` by default: the answer, abstention fields (`decision_abstained`, `abstain_reason`), cited sources and safety envelope are kept and the internal decision diagnostics are omitted. Pass `view="full"` or `debug=True` for the full response.
|
|
7
|
+
- Add `prefer_entity_match` (both search methods) and `include_profile` (`search_with_proof`), sent only when enabled; type `query_entities`, `profile` and `view` on the search envelope.
|
|
8
|
+
- `MemoryChat.send()` and `send_with_context()` accept optional `answer_style` (`"natural"`, the server default, or `"quote"`) and `no_information_reply`; they are sent only when set.
|
|
9
|
+
- Requires the server release of 2026-10-19 for the `/search/reason` compact view; the other options are accepted by current servers.
|
|
10
|
+
|
|
11
|
+
## 2.6.3 — 2026-10-14 (candidate memory_ids)
|
|
12
|
+
|
|
13
|
+
- Candidates may carry optional `memory_ids` (at most 50 ids of 1-255 characters) naming the exact memories that action applies; when the candidate becomes the decision's action, its outcome is credited to those memories.
|
|
14
|
+
- The advisor input snapshot (`decide_with_advice`) admits and validates `memory_ids` before any evidence read, callback or write; `proofloop.decide` documents the candidate shape.
|
|
15
|
+
- Requires the server release that credits outcomes only to memories bound to the action taken; a `proof_context` attests what was retrieved and no longer earns per-memory outcome credit by itself.
|
|
16
|
+
|
|
3
17
|
## 2.6.2 — 2026-10-06 (compact advice, batches and confirmation)
|
|
4
18
|
|
|
5
19
|
- Correct canonical documentation metadata and simplify the stable three-call guide.
|
|
6
20
|
- Add optional compact search/advice, bounded independently committed decision/outcome batches, and explicit caller-confirmation transports.
|
|
7
21
|
- Add bounded chat-learning wait options while preserving the original job receipt.
|
|
8
22
|
- Keep historical confidence scope aliases, ACT-as-advice and protected outcome boundaries intact.
|
|
9
|
-
- Requires matching
|
|
23
|
+
- Requires a matching server release; no benchmark superiority, latency SLA or automatic execution is implied.
|
|
10
24
|
|
|
11
25
|
## 2.6.1 — 2026-10-04 (advisory horizon helpers)
|
|
12
26
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hebbrix
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.7.0
|
|
4
4
|
Summary: Typed Python client for Hebbrix memory, retrieval, and outcome-learning APIs
|
|
5
5
|
Author-email: Hebbrix Team <support@hebbrix.com>
|
|
6
6
|
Maintainer-email: Hebbrix Team <support@hebbrix.com>
|
|
@@ -65,14 +65,14 @@ Dynamic: license-file
|
|
|
65
65
|
|
|
66
66
|
Typed Python client for Hebbrix memory, retrieval, and outcome-learning APIs.
|
|
67
67
|
|
|
68
|
-
The current stable package is **2.
|
|
68
|
+
The current stable package is **2.7.0**. Start with memory storage and search, or
|
|
69
69
|
the three-call outcome-learning workflow below. Advanced evidence and execution
|
|
70
70
|
permission transports are opt-in; installing the SDK never permits an action.
|
|
71
71
|
|
|
72
72
|
## Install
|
|
73
73
|
|
|
74
74
|
```bash
|
|
75
|
-
pip install hebbrix==2.
|
|
75
|
+
pip install hebbrix==2.7.0
|
|
76
76
|
```
|
|
77
77
|
|
|
78
78
|
Python 3.8+ is supported. `MemoryClient` is asynchronous. `SyncMemoryClient`
|
|
@@ -151,8 +151,21 @@ The canonical `gate` is `ASK`, `REVIEW`, `ACT`, or `BLOCK`. `BLOCK` dominates;
|
|
|
151
151
|
`ACT` is advice, not permission. Apply your own current authorization and human
|
|
152
152
|
approval policy before executing any tool. Do not infer permission from a score.
|
|
153
153
|
|
|
154
|
+
Version 2.7.0 makes search and reason responses compact by default: rows keep
|
|
155
|
+
content, identity, score, timestamps, confidence and entity scope (`applies_to`,
|
|
156
|
+
`entity_match`), and Reason keeps the answer, `decision_abstained`,
|
|
157
|
+
`abstain_reason` and cited sources. Pass `view="full"` or `debug=True` for
|
|
158
|
+
diagnostics. `search_with_proof(include_profile=True)` returns standing profile
|
|
159
|
+
facts and preferences, and `MemoryChat.send(answer_style="quote")` returns the
|
|
160
|
+
stored source text verbatim instead of a plain-language reply.
|
|
161
|
+
|
|
162
|
+
Version 2.6.3 lets each `proofloop.decide` candidate carry `memory_ids`: the exact
|
|
163
|
+
memories that action applies. When that candidate becomes the decision's action,
|
|
164
|
+
its outcome is credited to those memories; a `proof_context` attests what was
|
|
165
|
+
retrieved but never earns per-memory outcome credit by itself.
|
|
166
|
+
|
|
154
167
|
Version 2.6.2 adds compact advice, batch, and explicit confirmation helpers.
|
|
155
|
-
They require matching
|
|
168
|
+
They require a matching server release. Compact views retain scope and safety
|
|
156
169
|
caveats; complete receipts remain on the server.
|
|
157
170
|
|
|
158
171
|
## Durable readiness
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
Typed Python client for Hebbrix memory, retrieval, and outcome-learning APIs.
|
|
4
4
|
|
|
5
|
-
The current stable package is **2.
|
|
5
|
+
The current stable package is **2.7.0**. Start with memory storage and search, or
|
|
6
6
|
the three-call outcome-learning workflow below. Advanced evidence and execution
|
|
7
7
|
permission transports are opt-in; installing the SDK never permits an action.
|
|
8
8
|
|
|
9
9
|
## Install
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
pip install hebbrix==2.
|
|
12
|
+
pip install hebbrix==2.7.0
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Python 3.8+ is supported. `MemoryClient` is asynchronous. `SyncMemoryClient`
|
|
@@ -88,8 +88,21 @@ The canonical `gate` is `ASK`, `REVIEW`, `ACT`, or `BLOCK`. `BLOCK` dominates;
|
|
|
88
88
|
`ACT` is advice, not permission. Apply your own current authorization and human
|
|
89
89
|
approval policy before executing any tool. Do not infer permission from a score.
|
|
90
90
|
|
|
91
|
+
Version 2.7.0 makes search and reason responses compact by default: rows keep
|
|
92
|
+
content, identity, score, timestamps, confidence and entity scope (`applies_to`,
|
|
93
|
+
`entity_match`), and Reason keeps the answer, `decision_abstained`,
|
|
94
|
+
`abstain_reason` and cited sources. Pass `view="full"` or `debug=True` for
|
|
95
|
+
diagnostics. `search_with_proof(include_profile=True)` returns standing profile
|
|
96
|
+
facts and preferences, and `MemoryChat.send(answer_style="quote")` returns the
|
|
97
|
+
stored source text verbatim instead of a plain-language reply.
|
|
98
|
+
|
|
99
|
+
Version 2.6.3 lets each `proofloop.decide` candidate carry `memory_ids`: the exact
|
|
100
|
+
memories that action applies. When that candidate becomes the decision's action,
|
|
101
|
+
its outcome is credited to those memories; a `proof_context` attests what was
|
|
102
|
+
retrieved but never earns per-memory outcome credit by itself.
|
|
103
|
+
|
|
91
104
|
Version 2.6.2 adds compact advice, batch, and explicit confirmation helpers.
|
|
92
|
-
They require matching
|
|
105
|
+
They require a matching server release. Compact views retain scope and safety
|
|
93
106
|
caveats; complete receipts remain on the server.
|
|
94
107
|
|
|
95
108
|
## Durable readiness
|
|
@@ -121,9 +121,11 @@ def _snapshot_advisor_inputs(candidates, context):
|
|
|
121
121
|
"action_key",
|
|
122
122
|
"description",
|
|
123
123
|
"features",
|
|
124
|
+
"memory_ids",
|
|
124
125
|
}:
|
|
125
126
|
raise ValueError(
|
|
126
|
-
"candidates must contain only action_key, description
|
|
127
|
+
"candidates must contain only action_key, description, features "
|
|
128
|
+
"and memory_ids"
|
|
127
129
|
)
|
|
128
130
|
key = candidate.get("action_key")
|
|
129
131
|
if (
|
|
@@ -145,6 +147,18 @@ def _snapshot_advisor_inputs(candidates, context):
|
|
|
145
147
|
)
|
|
146
148
|
if "features" in candidate and type(candidate["features"]) is not dict:
|
|
147
149
|
raise ValueError("candidate features must be a JSON object")
|
|
150
|
+
memory_ids = candidate.get("memory_ids")
|
|
151
|
+
if memory_ids is not None and (
|
|
152
|
+
type(memory_ids) is not list
|
|
153
|
+
or len(memory_ids) > 50
|
|
154
|
+
or any(
|
|
155
|
+
type(memory_id) is not str or not 1 <= len(memory_id) <= 255
|
|
156
|
+
for memory_id in memory_ids
|
|
157
|
+
)
|
|
158
|
+
):
|
|
159
|
+
raise ValueError(
|
|
160
|
+
"candidate memory_ids must be a list of at most 50 memory ids"
|
|
161
|
+
)
|
|
148
162
|
keys.append(key)
|
|
149
163
|
return candidates_copy, context_copy, keys
|
|
150
164
|
|
|
@@ -32,6 +32,34 @@ def _chat_readiness_options(wait_for_learning_seconds, include_low_confidence):
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
|
|
35
|
+
_ANSWER_STYLES = ("natural", "quote")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _chat_answer_options(answer_style, no_information_reply):
|
|
39
|
+
"""Optional reply-phrasing fields; omitted unless set (server defaults apply).
|
|
40
|
+
|
|
41
|
+
answer_style: "natural" (server default) composes a plain reply from the
|
|
42
|
+
stored sources a personal question is answered from; "quote" returns that
|
|
43
|
+
source text verbatim. no_information_reply replaces the default reply
|
|
44
|
+
("I don't know that yet.") when no stored source answers.
|
|
45
|
+
"""
|
|
46
|
+
if answer_style is not None and answer_style not in _ANSWER_STYLES:
|
|
47
|
+
raise ValueError("answer_style must be 'natural' or 'quote'")
|
|
48
|
+
if no_information_reply is not None and (
|
|
49
|
+
not isinstance(no_information_reply, str)
|
|
50
|
+
or not 1 <= len(no_information_reply) <= 500
|
|
51
|
+
):
|
|
52
|
+
raise ValueError("no_information_reply must be 1-500 characters")
|
|
53
|
+
return {
|
|
54
|
+
**({"answer_style": answer_style} if answer_style is not None else {}),
|
|
55
|
+
**(
|
|
56
|
+
{"no_information_reply": no_information_reply}
|
|
57
|
+
if no_information_reply is not None
|
|
58
|
+
else {}
|
|
59
|
+
),
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
35
63
|
class MemoryChat:
|
|
36
64
|
"""
|
|
37
65
|
Simple chat interface with automatic memory integration.
|
|
@@ -112,6 +140,8 @@ class MemoryChat:
|
|
|
112
140
|
max_tokens: Optional[int] = None,
|
|
113
141
|
*, wait_for_learning_seconds: float = 0,
|
|
114
142
|
include_low_confidence: bool = False,
|
|
143
|
+
answer_style: Optional[str] = None,
|
|
144
|
+
no_information_reply: Optional[str] = None,
|
|
115
145
|
) -> str:
|
|
116
146
|
"""
|
|
117
147
|
Send a message and get a response with automatic memory integration.
|
|
@@ -123,6 +153,12 @@ class MemoryChat:
|
|
|
123
153
|
session_id: Session identifier (user ID or conversation ID)
|
|
124
154
|
temperature: Response randomness (0-2, default: 0.7)
|
|
125
155
|
max_tokens: Maximum response length (default: None)
|
|
156
|
+
answer_style: "natural" (server default) or "quote" (verbatim
|
|
157
|
+
stored source text) for personal questions answered from memory
|
|
158
|
+
no_information_reply: Reply when no stored source answers
|
|
159
|
+
answer_style: "natural" (server default) or "quote" (verbatim
|
|
160
|
+
stored source text) for personal questions answered from memory
|
|
161
|
+
no_information_reply: Reply when no stored source answers
|
|
126
162
|
|
|
127
163
|
Returns:
|
|
128
164
|
Assistant's response as a string
|
|
@@ -141,6 +177,7 @@ class MemoryChat:
|
|
|
141
177
|
"memory_limit": self.memory_limit,
|
|
142
178
|
"temperature": temperature,
|
|
143
179
|
**_chat_readiness_options(wait_for_learning_seconds, include_low_confidence),
|
|
180
|
+
**_chat_answer_options(answer_style, no_information_reply),
|
|
144
181
|
}
|
|
145
182
|
|
|
146
183
|
if max_tokens:
|
|
@@ -167,6 +204,8 @@ class MemoryChat:
|
|
|
167
204
|
max_tokens: Optional[int] = None,
|
|
168
205
|
*, wait_for_learning_seconds: float = 0,
|
|
169
206
|
include_low_confidence: bool = False,
|
|
207
|
+
answer_style: Optional[str] = None,
|
|
208
|
+
no_information_reply: Optional[str] = None,
|
|
170
209
|
) -> Dict[str, Any]:
|
|
171
210
|
"""
|
|
172
211
|
Send a message and get full response with memory context.
|
|
@@ -178,6 +217,12 @@ class MemoryChat:
|
|
|
178
217
|
session_id: Session identifier
|
|
179
218
|
temperature: Response randomness (0-2, default: 0.7)
|
|
180
219
|
max_tokens: Maximum response length (default: None)
|
|
220
|
+
answer_style: "natural" (server default) or "quote" (verbatim
|
|
221
|
+
stored source text) for personal questions answered from memory
|
|
222
|
+
no_information_reply: Reply when no stored source answers
|
|
223
|
+
answer_style: "natural" (server default) or "quote" (verbatim
|
|
224
|
+
stored source text) for personal questions answered from memory
|
|
225
|
+
no_information_reply: Reply when no stored source answers
|
|
181
226
|
|
|
182
227
|
Returns:
|
|
183
228
|
Full response dict with memory context
|
|
@@ -198,6 +243,7 @@ class MemoryChat:
|
|
|
198
243
|
"memory_limit": self.memory_limit,
|
|
199
244
|
"temperature": temperature,
|
|
200
245
|
**_chat_readiness_options(wait_for_learning_seconds, include_low_confidence),
|
|
246
|
+
**_chat_answer_options(answer_style, no_information_reply),
|
|
201
247
|
}
|
|
202
248
|
|
|
203
249
|
if max_tokens:
|
|
@@ -93,7 +93,7 @@ class MemoryClient:
|
|
|
93
93
|
"""Get request headers."""
|
|
94
94
|
headers = {
|
|
95
95
|
"Content-Type": "application/json",
|
|
96
|
-
"User-Agent": "hebbrix-python/2.
|
|
96
|
+
"User-Agent": "hebbrix-python/2.7.0",
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
if self.api_key:
|
|
@@ -332,6 +332,8 @@ class MemoryClient:
|
|
|
332
332
|
agent_id: Optional[str] = None,
|
|
333
333
|
run_id: Optional[str] = None,
|
|
334
334
|
facets: Optional[List[str]] = None,
|
|
335
|
+
view: str = "compact",
|
|
336
|
+
debug: bool = False,
|
|
335
337
|
) -> Dict[str, Any]:
|
|
336
338
|
"""
|
|
337
339
|
Perform reasoning over memories.
|
|
@@ -345,6 +347,8 @@ class MemoryClient:
|
|
|
345
347
|
agent_id: Agent isolation scope
|
|
346
348
|
run_id: Run isolation scope
|
|
347
349
|
facets: Optional typed decomposition facets
|
|
350
|
+
view: ``"compact"`` (default) or ``"full"`` diagnostics
|
|
351
|
+
debug: Return the full diagnostic response
|
|
348
352
|
|
|
349
353
|
Returns:
|
|
350
354
|
Reasoning result with answer and sources
|
|
@@ -358,6 +362,8 @@ class MemoryClient:
|
|
|
358
362
|
agent_id=agent_id,
|
|
359
363
|
run_id=run_id,
|
|
360
364
|
facets=facets,
|
|
365
|
+
view=view,
|
|
366
|
+
debug=debug,
|
|
361
367
|
)
|
|
362
368
|
|
|
363
369
|
async def close(self):
|
|
@@ -53,3 +53,9 @@ class SearchSafetyEnvelope(TypedDict, total=False):
|
|
|
53
53
|
degraded: bool
|
|
54
54
|
sdk_safety_reason: str
|
|
55
55
|
proof_context: Dict[str, Any]
|
|
56
|
+
# Entities the query names; each row carries applies_to/entity_match.
|
|
57
|
+
query_entities: List[str]
|
|
58
|
+
# Standing profile facts and preferences (search include_profile=True).
|
|
59
|
+
profile: Dict[str, Any]
|
|
60
|
+
# "compact" (SDK default) or absent for the full projection.
|
|
61
|
+
view: str
|
|
@@ -992,6 +992,101 @@ class CorrectionsResource(BaseResource):
|
|
|
992
992
|
return await self.client.delete(f"/v1/corrections/{correction_id}")
|
|
993
993
|
|
|
994
994
|
|
|
995
|
+
def _search_payload(
|
|
996
|
+
query: str,
|
|
997
|
+
*,
|
|
998
|
+
collection_id: Optional[str] = None,
|
|
999
|
+
limit: int = 10,
|
|
1000
|
+
search_type: str = "hybrid",
|
|
1001
|
+
filters: Optional[Dict[str, Any]] = None,
|
|
1002
|
+
user_id: Optional[str] = None,
|
|
1003
|
+
agent_id: Optional[str] = None,
|
|
1004
|
+
run_id: Optional[str] = None,
|
|
1005
|
+
fast: Optional[bool] = None,
|
|
1006
|
+
threshold: Optional[float] = None,
|
|
1007
|
+
include_low_confidence: bool = False,
|
|
1008
|
+
group_by_source: bool = True,
|
|
1009
|
+
debug: bool = False,
|
|
1010
|
+
view: Optional[str] = "compact",
|
|
1011
|
+
include_proof_context: Optional[bool] = None,
|
|
1012
|
+
prefer_entity_match: bool = False,
|
|
1013
|
+
include_profile: bool = False,
|
|
1014
|
+
) -> Dict[str, Any]:
|
|
1015
|
+
"""Build the identical /v1/search body for sync and async clients.
|
|
1016
|
+
|
|
1017
|
+
``view`` defaults to ``"compact"``: rows keep content, identity, score,
|
|
1018
|
+
rank, timestamps, confidence and entity scope, and the safety envelope and
|
|
1019
|
+
proof context are unchanged; ranking diagnostics are omitted unless
|
|
1020
|
+
``debug=True``. Pass ``view="full"`` for the complete per-row metadata.
|
|
1021
|
+
New opt-in fields are sent only when enabled, so older servers that
|
|
1022
|
+
reject unknown fields keep accepting default requests.
|
|
1023
|
+
"""
|
|
1024
|
+
|
|
1025
|
+
_validate_advice_view(view)
|
|
1026
|
+
payload: Dict[str, Any] = {
|
|
1027
|
+
"query": query,
|
|
1028
|
+
"collection_id": collection_id,
|
|
1029
|
+
"user_id": user_id,
|
|
1030
|
+
"agent_id": agent_id,
|
|
1031
|
+
"run_id": run_id,
|
|
1032
|
+
"limit": limit,
|
|
1033
|
+
"search_type": search_type,
|
|
1034
|
+
"filters": filters or {},
|
|
1035
|
+
"include_low_confidence": include_low_confidence,
|
|
1036
|
+
"group_by_source": group_by_source,
|
|
1037
|
+
"debug": debug,
|
|
1038
|
+
"view": view,
|
|
1039
|
+
"include_proof_context": include_proof_context,
|
|
1040
|
+
}
|
|
1041
|
+
if fast is not None:
|
|
1042
|
+
payload["fast"] = fast
|
|
1043
|
+
if threshold is not None:
|
|
1044
|
+
payload["threshold"] = threshold
|
|
1045
|
+
if prefer_entity_match:
|
|
1046
|
+
payload["prefer_entity_match"] = True
|
|
1047
|
+
if include_profile:
|
|
1048
|
+
payload["include_profile"] = True
|
|
1049
|
+
return {key: value for key, value in payload.items() if value is not None}
|
|
1050
|
+
|
|
1051
|
+
|
|
1052
|
+
def _reason_payload(
|
|
1053
|
+
query: str,
|
|
1054
|
+
*,
|
|
1055
|
+
collection_id: Optional[str] = None,
|
|
1056
|
+
provider: Optional[str] = None,
|
|
1057
|
+
include_steps: bool = False,
|
|
1058
|
+
user_id: Optional[str] = None,
|
|
1059
|
+
agent_id: Optional[str] = None,
|
|
1060
|
+
run_id: Optional[str] = None,
|
|
1061
|
+
facets: Optional[List[str]] = None,
|
|
1062
|
+
view: str = "compact",
|
|
1063
|
+
debug: bool = False,
|
|
1064
|
+
) -> Dict[str, Any]:
|
|
1065
|
+
"""Build the identical /v1/search/reason body for sync and async clients.
|
|
1066
|
+
|
|
1067
|
+
``view`` defaults to ``"compact"``: the answer, abstention fields, cited
|
|
1068
|
+
sources and safety envelope are kept and the internal decision
|
|
1069
|
+
diagnostics are omitted; ``view="full"`` or ``debug=True`` returns them.
|
|
1070
|
+
"""
|
|
1071
|
+
|
|
1072
|
+
if view not in {"compact", "full"}:
|
|
1073
|
+
raise ValueError("view must be 'compact' or 'full'")
|
|
1074
|
+
payload: Dict[str, Any] = {
|
|
1075
|
+
"query": query,
|
|
1076
|
+
"collection_id": collection_id,
|
|
1077
|
+
"provider": provider,
|
|
1078
|
+
"include_steps": include_steps,
|
|
1079
|
+
"user_id": user_id,
|
|
1080
|
+
"agent_id": agent_id,
|
|
1081
|
+
"run_id": run_id,
|
|
1082
|
+
"facets": facets or [],
|
|
1083
|
+
"view": view,
|
|
1084
|
+
}
|
|
1085
|
+
if debug:
|
|
1086
|
+
payload["debug"] = True
|
|
1087
|
+
return payload
|
|
1088
|
+
|
|
1089
|
+
|
|
995
1090
|
class SearchResource(BaseResource):
|
|
996
1091
|
"""Search and reasoning endpoints."""
|
|
997
1092
|
|
|
@@ -1010,7 +1105,9 @@ class SearchResource(BaseResource):
|
|
|
1010
1105
|
include_low_confidence: bool = False,
|
|
1011
1106
|
group_by_source: bool = True,
|
|
1012
1107
|
debug: bool = False,
|
|
1013
|
-
view: Optional[str] =
|
|
1108
|
+
view: Optional[str] = "compact",
|
|
1109
|
+
include_proof_context: Optional[bool] = False,
|
|
1110
|
+
prefer_entity_match: bool = False,
|
|
1014
1111
|
) -> List[Dict[str, Any]]:
|
|
1015
1112
|
"""
|
|
1016
1113
|
Search memories.
|
|
@@ -1021,6 +1118,11 @@ class SearchResource(BaseResource):
|
|
|
1021
1118
|
limit: Number of results
|
|
1022
1119
|
search_type: Type of search (hybrid, vector, bm25, graph)
|
|
1023
1120
|
filters: Additional filters
|
|
1121
|
+
view: ``"compact"`` (default) or ``"full"`` per-row metadata
|
|
1122
|
+
include_proof_context: This method returns only the rows, so the
|
|
1123
|
+
ProofLoop token is not requested by default; use
|
|
1124
|
+
``search_with_proof`` to keep it.
|
|
1125
|
+
prefer_entity_match: Rank rows naming the query's entities first.
|
|
1024
1126
|
|
|
1025
1127
|
Returns:
|
|
1026
1128
|
Search results
|
|
@@ -1040,6 +1142,8 @@ class SearchResource(BaseResource):
|
|
|
1040
1142
|
group_by_source=group_by_source,
|
|
1041
1143
|
debug=debug,
|
|
1042
1144
|
view=view,
|
|
1145
|
+
include_proof_context=include_proof_context,
|
|
1146
|
+
prefer_entity_match=prefer_entity_match,
|
|
1043
1147
|
)
|
|
1044
1148
|
return response["results"]
|
|
1045
1149
|
|
|
@@ -1058,36 +1162,41 @@ class SearchResource(BaseResource):
|
|
|
1058
1162
|
include_low_confidence: bool = False,
|
|
1059
1163
|
group_by_source: bool = True,
|
|
1060
1164
|
debug: bool = False,
|
|
1061
|
-
view: Optional[str] =
|
|
1165
|
+
view: Optional[str] = "compact",
|
|
1166
|
+
include_proof_context: Optional[bool] = None,
|
|
1167
|
+
prefer_entity_match: bool = False,
|
|
1168
|
+
include_profile: bool = False,
|
|
1062
1169
|
) -> SearchSafetyEnvelope:
|
|
1063
1170
|
"""Search and preserve the automatic ProofLoop context.
|
|
1064
1171
|
|
|
1065
1172
|
The legacy ``search`` method still returns only the result list. Use
|
|
1066
|
-
this method when the selected evidence will feed a learned decision
|
|
1173
|
+
this method when the selected evidence will feed a learned decision,
|
|
1174
|
+
or to read ``profile`` (``include_profile=True``) and
|
|
1175
|
+
``query_entities``. Responses are compact by default; pass
|
|
1176
|
+
``view="full"`` or ``debug=True`` for diagnostics.
|
|
1067
1177
|
"""
|
|
1068
1178
|
|
|
1069
|
-
_validate_advice_view(view)
|
|
1070
|
-
payload: Dict[str, Any] = {
|
|
1071
|
-
"query": query,
|
|
1072
|
-
"collection_id": collection_id,
|
|
1073
|
-
"user_id": user_id,
|
|
1074
|
-
"agent_id": agent_id,
|
|
1075
|
-
"run_id": run_id,
|
|
1076
|
-
"limit": limit,
|
|
1077
|
-
"search_type": search_type,
|
|
1078
|
-
"filters": filters or {},
|
|
1079
|
-
"include_low_confidence": include_low_confidence,
|
|
1080
|
-
"group_by_source": group_by_source,
|
|
1081
|
-
"debug": debug,
|
|
1082
|
-
"view": view,
|
|
1083
|
-
}
|
|
1084
|
-
if fast is not None:
|
|
1085
|
-
payload["fast"] = fast
|
|
1086
|
-
if threshold is not None:
|
|
1087
|
-
payload["threshold"] = threshold
|
|
1088
1179
|
response = await self.client.post(
|
|
1089
1180
|
"/v1/search",
|
|
1090
|
-
json=
|
|
1181
|
+
json=_search_payload(
|
|
1182
|
+
query,
|
|
1183
|
+
collection_id=collection_id,
|
|
1184
|
+
limit=limit,
|
|
1185
|
+
search_type=search_type,
|
|
1186
|
+
filters=filters,
|
|
1187
|
+
user_id=user_id,
|
|
1188
|
+
agent_id=agent_id,
|
|
1189
|
+
run_id=run_id,
|
|
1190
|
+
fast=fast,
|
|
1191
|
+
threshold=threshold,
|
|
1192
|
+
include_low_confidence=include_low_confidence,
|
|
1193
|
+
group_by_source=group_by_source,
|
|
1194
|
+
debug=debug,
|
|
1195
|
+
view=view,
|
|
1196
|
+
include_proof_context=include_proof_context,
|
|
1197
|
+
prefer_entity_match=prefer_entity_match,
|
|
1198
|
+
include_profile=include_profile,
|
|
1199
|
+
),
|
|
1091
1200
|
)
|
|
1092
1201
|
return _canonical_search_envelope(response)
|
|
1093
1202
|
|
|
@@ -1123,6 +1232,8 @@ class SearchResource(BaseResource):
|
|
|
1123
1232
|
agent_id: Optional[str] = None,
|
|
1124
1233
|
run_id: Optional[str] = None,
|
|
1125
1234
|
facets: Optional[List[str]] = None,
|
|
1235
|
+
view: str = "compact",
|
|
1236
|
+
debug: bool = False,
|
|
1126
1237
|
) -> SearchSafetyEnvelope:
|
|
1127
1238
|
"""
|
|
1128
1239
|
Perform reasoning over memories.
|
|
@@ -1136,22 +1247,26 @@ class SearchResource(BaseResource):
|
|
|
1136
1247
|
agent_id: Agent isolation scope
|
|
1137
1248
|
run_id: Run isolation scope
|
|
1138
1249
|
facets: Optional typed decomposition facets
|
|
1250
|
+
view: ``"compact"`` (default) or ``"full"`` diagnostics
|
|
1251
|
+
debug: Return the full diagnostic response
|
|
1139
1252
|
|
|
1140
1253
|
Returns:
|
|
1141
1254
|
Reasoning result with answer and sources
|
|
1142
1255
|
"""
|
|
1143
1256
|
response = await self.client.post(
|
|
1144
1257
|
"/v1/search/reason",
|
|
1145
|
-
json=
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1258
|
+
json=_reason_payload(
|
|
1259
|
+
query,
|
|
1260
|
+
collection_id=collection_id,
|
|
1261
|
+
provider=provider,
|
|
1262
|
+
include_steps=include_steps,
|
|
1263
|
+
user_id=user_id,
|
|
1264
|
+
agent_id=agent_id,
|
|
1265
|
+
run_id=run_id,
|
|
1266
|
+
facets=facets,
|
|
1267
|
+
view=view,
|
|
1268
|
+
debug=debug,
|
|
1269
|
+
),
|
|
1155
1270
|
)
|
|
1156
1271
|
return _canonical_search_envelope(response, rows_key="sources")
|
|
1157
1272
|
|
|
@@ -1170,6 +1285,15 @@ class ProofLoopResource(BaseResource):
|
|
|
1170
1285
|
idempotency_key: Optional[str] = None,
|
|
1171
1286
|
**kwargs,
|
|
1172
1287
|
) -> Dict[str, Any]:
|
|
1288
|
+
"""Log one decision among 1-50 candidate actions.
|
|
1289
|
+
|
|
1290
|
+
Each candidate is ``{"action_key": str, "description"?: str,
|
|
1291
|
+
"features"?: dict, "memory_ids"?: list[str]}``. ``memory_ids`` (at most
|
|
1292
|
+
50) names the exact memories the action applies, such as its runbook;
|
|
1293
|
+
when that candidate becomes the decision's action, its outcome is
|
|
1294
|
+
credited to those memories. A ``proof_context`` attests what was
|
|
1295
|
+
retrieved but never earns per-memory outcome credit by itself.
|
|
1296
|
+
"""
|
|
1173
1297
|
token = (
|
|
1174
1298
|
proof_context.get("token")
|
|
1175
1299
|
if isinstance(proof_context, dict)
|
|
@@ -1633,6 +1757,9 @@ class RLResource(BaseResource):
|
|
|
1633
1757
|
"""
|
|
1634
1758
|
Train the Memory Manager agent using RL.
|
|
1635
1759
|
|
|
1760
|
+
Operator-only: the server requires a Hebbrix administrator session and
|
|
1761
|
+
refuses API keys with 403, so this route is not in the public OpenAPI.
|
|
1762
|
+
|
|
1636
1763
|
Args:
|
|
1637
1764
|
num_episodes: Number of training episodes
|
|
1638
1765
|
**kwargs: Additional training parameters
|
|
@@ -1656,6 +1783,9 @@ class RLResource(BaseResource):
|
|
|
1656
1783
|
"""
|
|
1657
1784
|
Train the Answer Agent using RL.
|
|
1658
1785
|
|
|
1786
|
+
Operator-only: the server requires a Hebbrix administrator session and
|
|
1787
|
+
refuses API keys with 403, so this route is not in the public OpenAPI.
|
|
1788
|
+
|
|
1659
1789
|
Args:
|
|
1660
1790
|
num_episodes: Number of training episodes
|
|
1661
1791
|
**kwargs: Additional training parameters
|
|
@@ -17,7 +17,12 @@ from hebbrix.exceptions import (
|
|
|
17
17
|
ServerError,
|
|
18
18
|
ValidationError,
|
|
19
19
|
)
|
|
20
|
-
from hebbrix.resources import
|
|
20
|
+
from hebbrix.resources import (
|
|
21
|
+
_canonical_search_envelope,
|
|
22
|
+
_memory_create_payload,
|
|
23
|
+
_reason_payload,
|
|
24
|
+
_search_payload,
|
|
25
|
+
)
|
|
21
26
|
from hebbrix._advice import _snapshot_advisor_inputs, _validated_advisor_selection, _validate_advisor_scope, _validate_advisor_horizon
|
|
22
27
|
|
|
23
28
|
|
|
@@ -576,35 +581,39 @@ class SyncSearchResource:
|
|
|
576
581
|
include_low_confidence: bool = False,
|
|
577
582
|
group_by_source: bool = True,
|
|
578
583
|
debug: bool = False,
|
|
579
|
-
view: Optional[str] =
|
|
584
|
+
view: Optional[str] = "compact",
|
|
585
|
+
include_proof_context: Optional[bool] = None,
|
|
586
|
+
prefer_entity_match: bool = False,
|
|
587
|
+
include_profile: bool = False,
|
|
580
588
|
) -> Dict[str, Any]:
|
|
581
|
-
from ._advice import _validate_advice_view
|
|
582
|
-
_validate_advice_view(view)
|
|
583
|
-
payload: Dict[str, Any] = {
|
|
584
|
-
"query": query,
|
|
585
|
-
"collection_id": collection_id,
|
|
586
|
-
"user_id": user_id,
|
|
587
|
-
"agent_id": agent_id,
|
|
588
|
-
"run_id": run_id,
|
|
589
|
-
"limit": limit,
|
|
590
|
-
"search_type": search_type,
|
|
591
|
-
"filters": filters or {},
|
|
592
|
-
"include_low_confidence": include_low_confidence,
|
|
593
|
-
"group_by_source": group_by_source,
|
|
594
|
-
"debug": debug,
|
|
595
|
-
"view": view,
|
|
596
|
-
}
|
|
597
|
-
if fast is not None:
|
|
598
|
-
payload["fast"] = fast
|
|
599
|
-
if threshold is not None:
|
|
600
|
-
payload["threshold"] = threshold
|
|
601
589
|
response = self.client.post(
|
|
602
590
|
"/v1/search",
|
|
603
|
-
json=
|
|
591
|
+
json=_search_payload(
|
|
592
|
+
query,
|
|
593
|
+
collection_id=collection_id,
|
|
594
|
+
limit=limit,
|
|
595
|
+
search_type=search_type,
|
|
596
|
+
filters=filters,
|
|
597
|
+
user_id=user_id,
|
|
598
|
+
agent_id=agent_id,
|
|
599
|
+
run_id=run_id,
|
|
600
|
+
fast=fast,
|
|
601
|
+
threshold=threshold,
|
|
602
|
+
include_low_confidence=include_low_confidence,
|
|
603
|
+
group_by_source=group_by_source,
|
|
604
|
+
debug=debug,
|
|
605
|
+
view=view,
|
|
606
|
+
include_proof_context=include_proof_context,
|
|
607
|
+
prefer_entity_match=prefer_entity_match,
|
|
608
|
+
include_profile=include_profile,
|
|
609
|
+
),
|
|
604
610
|
)
|
|
605
611
|
return _canonical_search_envelope(response)
|
|
606
612
|
|
|
607
613
|
def search(self, *args, **kwargs) -> List[Dict[str, Any]]:
|
|
614
|
+
# Only the rows are returned, so the proof token is not requested
|
|
615
|
+
# unless the caller asks for it (parity with the async client).
|
|
616
|
+
kwargs.setdefault("include_proof_context", False)
|
|
608
617
|
return self.search_with_proof(*args, **kwargs).get("results", [])
|
|
609
618
|
|
|
610
619
|
def reason(
|
|
@@ -617,19 +626,23 @@ class SyncSearchResource:
|
|
|
617
626
|
agent_id: Optional[str] = None,
|
|
618
627
|
run_id: Optional[str] = None,
|
|
619
628
|
facets: Optional[List[str]] = None,
|
|
629
|
+
view: str = "compact",
|
|
630
|
+
debug: bool = False,
|
|
620
631
|
) -> Dict[str, Any]:
|
|
621
632
|
response = self.client.post(
|
|
622
633
|
"/v1/search/reason",
|
|
623
|
-
json=
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
634
|
+
json=_reason_payload(
|
|
635
|
+
query,
|
|
636
|
+
collection_id=collection_id,
|
|
637
|
+
provider=provider,
|
|
638
|
+
include_steps=include_steps,
|
|
639
|
+
user_id=user_id,
|
|
640
|
+
agent_id=agent_id,
|
|
641
|
+
run_id=run_id,
|
|
642
|
+
facets=facets,
|
|
643
|
+
view=view,
|
|
644
|
+
debug=debug,
|
|
645
|
+
),
|
|
633
646
|
)
|
|
634
647
|
return _canonical_search_envelope(response, rows_key="sources")
|
|
635
648
|
|
|
@@ -649,6 +662,15 @@ class SyncProofLoopResource:
|
|
|
649
662
|
idempotency_key: Optional[str] = None,
|
|
650
663
|
**kwargs,
|
|
651
664
|
) -> Dict[str, Any]:
|
|
665
|
+
"""Log one decision among 1-50 candidate actions.
|
|
666
|
+
|
|
667
|
+
Each candidate is ``{"action_key": str, "description"?: str,
|
|
668
|
+
"features"?: dict, "memory_ids"?: list[str]}``. ``memory_ids`` (at most
|
|
669
|
+
50) names the exact memories the action applies, such as its runbook;
|
|
670
|
+
when that candidate becomes the decision's action, its outcome is
|
|
671
|
+
credited to those memories. A ``proof_context`` attests what was
|
|
672
|
+
retrieved but never earns per-memory outcome credit by itself.
|
|
673
|
+
"""
|
|
652
674
|
token = (
|
|
653
675
|
proof_context.get("token")
|
|
654
676
|
if isinstance(proof_context, dict)
|
|
@@ -1110,7 +1132,7 @@ class SyncMemoryClient:
|
|
|
1110
1132
|
self.source = source or os.getenv("HEBBRIX_SOURCE")
|
|
1111
1133
|
headers = {
|
|
1112
1134
|
"Content-Type": "application/json",
|
|
1113
|
-
"User-Agent": "hebbrix-python/2.
|
|
1135
|
+
"User-Agent": "hebbrix-python/2.7.0",
|
|
1114
1136
|
}
|
|
1115
1137
|
if api_key:
|
|
1116
1138
|
headers["Authorization"] = f"Bearer {api_key}"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hebbrix
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.7.0
|
|
4
4
|
Summary: Typed Python client for Hebbrix memory, retrieval, and outcome-learning APIs
|
|
5
5
|
Author-email: Hebbrix Team <support@hebbrix.com>
|
|
6
6
|
Maintainer-email: Hebbrix Team <support@hebbrix.com>
|
|
@@ -65,14 +65,14 @@ Dynamic: license-file
|
|
|
65
65
|
|
|
66
66
|
Typed Python client for Hebbrix memory, retrieval, and outcome-learning APIs.
|
|
67
67
|
|
|
68
|
-
The current stable package is **2.
|
|
68
|
+
The current stable package is **2.7.0**. Start with memory storage and search, or
|
|
69
69
|
the three-call outcome-learning workflow below. Advanced evidence and execution
|
|
70
70
|
permission transports are opt-in; installing the SDK never permits an action.
|
|
71
71
|
|
|
72
72
|
## Install
|
|
73
73
|
|
|
74
74
|
```bash
|
|
75
|
-
pip install hebbrix==2.
|
|
75
|
+
pip install hebbrix==2.7.0
|
|
76
76
|
```
|
|
77
77
|
|
|
78
78
|
Python 3.8+ is supported. `MemoryClient` is asynchronous. `SyncMemoryClient`
|
|
@@ -151,8 +151,21 @@ The canonical `gate` is `ASK`, `REVIEW`, `ACT`, or `BLOCK`. `BLOCK` dominates;
|
|
|
151
151
|
`ACT` is advice, not permission. Apply your own current authorization and human
|
|
152
152
|
approval policy before executing any tool. Do not infer permission from a score.
|
|
153
153
|
|
|
154
|
+
Version 2.7.0 makes search and reason responses compact by default: rows keep
|
|
155
|
+
content, identity, score, timestamps, confidence and entity scope (`applies_to`,
|
|
156
|
+
`entity_match`), and Reason keeps the answer, `decision_abstained`,
|
|
157
|
+
`abstain_reason` and cited sources. Pass `view="full"` or `debug=True` for
|
|
158
|
+
diagnostics. `search_with_proof(include_profile=True)` returns standing profile
|
|
159
|
+
facts and preferences, and `MemoryChat.send(answer_style="quote")` returns the
|
|
160
|
+
stored source text verbatim instead of a plain-language reply.
|
|
161
|
+
|
|
162
|
+
Version 2.6.3 lets each `proofloop.decide` candidate carry `memory_ids`: the exact
|
|
163
|
+
memories that action applies. When that candidate becomes the decision's action,
|
|
164
|
+
its outcome is credited to those memories; a `proof_context` attests what was
|
|
165
|
+
retrieved but never earns per-memory outcome credit by itself.
|
|
166
|
+
|
|
154
167
|
Version 2.6.2 adds compact advice, batch, and explicit confirmation helpers.
|
|
155
|
-
They require matching
|
|
168
|
+
They require a matching server release. Compact views retain scope and safety
|
|
156
169
|
caveats; complete receipts remain on the server.
|
|
157
170
|
|
|
158
171
|
## Durable readiness
|
|
@@ -27,6 +27,8 @@ hebbrix.egg-info/requires.txt
|
|
|
27
27
|
hebbrix.egg-info/top_level.txt
|
|
28
28
|
tests/test_advanced_resources.py
|
|
29
29
|
tests/test_advisor_integrity.py
|
|
30
|
+
tests/test_chat_answer_options.py
|
|
31
|
+
tests/test_compact_search_default.py
|
|
30
32
|
tests/test_customer_developer_experience.py
|
|
31
33
|
tests/test_evidence_loop_resource.py
|
|
32
34
|
tests/test_evidence_safety_boundary.py
|
|
@@ -274,3 +274,33 @@ async def test_cyclic_json_is_refused_before_read(kind):
|
|
|
274
274
|
with pytest.raises(ValueError):
|
|
275
275
|
await invoke(kind, client, params, advisor)
|
|
276
276
|
client.get.assert_not_called()
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
@pytest.mark.asyncio
|
|
280
|
+
@pytest.mark.parametrize("kind", ["sync", "async"])
|
|
281
|
+
async def test_candidate_memory_ids_reach_the_decision_unchanged(kind):
|
|
282
|
+
"""A candidate may bind the memories its action applies (outcome credit)."""
|
|
283
|
+
client = transport(kind)
|
|
284
|
+
params = inputs()
|
|
285
|
+
params["candidates"][1]["memory_ids"] = ["runbook-memory", "checklist-memory"]
|
|
286
|
+
await invoke(kind, client, params, Mock(return_value=choice()))
|
|
287
|
+
candidates = client.post.call_args.kwargs["json"]["candidates"]
|
|
288
|
+
assert candidates[1]["memory_ids"] == ["runbook-memory", "checklist-memory"]
|
|
289
|
+
assert "memory_ids" not in candidates[0]
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
@pytest.mark.asyncio
|
|
293
|
+
@pytest.mark.parametrize("kind", ["sync", "async"])
|
|
294
|
+
@pytest.mark.parametrize(
|
|
295
|
+
"memory_ids",
|
|
296
|
+
["runbook-memory", [""], ["x" * 256], [7], ["m"] * 51, {"id": "m"}],
|
|
297
|
+
)
|
|
298
|
+
async def test_invalid_candidate_memory_ids_fail_before_reads(kind, memory_ids):
|
|
299
|
+
client, advisor = transport(kind), Mock(return_value=choice())
|
|
300
|
+
params = inputs()
|
|
301
|
+
params["candidates"][0]["memory_ids"] = memory_ids
|
|
302
|
+
with pytest.raises(ValueError):
|
|
303
|
+
await invoke(kind, client, params, advisor)
|
|
304
|
+
client.get.assert_not_called()
|
|
305
|
+
client.post.assert_not_called()
|
|
306
|
+
advisor.assert_not_called()
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""MemoryChat exposes the reply-phrasing fields of /v1/chat/completions.
|
|
2
|
+
|
|
3
|
+
answer_style ("natural" by default on the server, or "quote") and
|
|
4
|
+
no_information_reply are sent only when set, so the server defaults apply and
|
|
5
|
+
older servers see the same request as before.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from unittest.mock import Mock, patch
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
from hebbrix.chat import MemoryChat
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _response():
|
|
15
|
+
response = Mock()
|
|
16
|
+
response.json.return_value = {
|
|
17
|
+
"choices": [{"message": {"content": "You live in Varnhold."}}],
|
|
18
|
+
"memory_context": {"answer_composition": "composed_from_sources"},
|
|
19
|
+
"usage": {},
|
|
20
|
+
"model": "test",
|
|
21
|
+
}
|
|
22
|
+
return response
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@pytest.mark.parametrize("method", ["send", "send_with_context"])
|
|
26
|
+
def test_answer_options_are_sent_only_when_set(method):
|
|
27
|
+
chat = MemoryChat(api_key="local-test", model="test")
|
|
28
|
+
with patch("hebbrix.chat.requests.post", return_value=_response()) as post:
|
|
29
|
+
getattr(chat, method)("Where do I live?", "s")
|
|
30
|
+
getattr(chat, method)(
|
|
31
|
+
"Where do I live?",
|
|
32
|
+
"s",
|
|
33
|
+
answer_style="quote",
|
|
34
|
+
no_information_reply="Not on file yet.",
|
|
35
|
+
)
|
|
36
|
+
default, explicit = (call.kwargs["json"] for call in post.call_args_list)
|
|
37
|
+
assert "answer_style" not in default and "no_information_reply" not in default
|
|
38
|
+
assert explicit["answer_style"] == "quote"
|
|
39
|
+
assert explicit["no_information_reply"] == "Not on file yet."
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@pytest.mark.parametrize(
|
|
43
|
+
"options",
|
|
44
|
+
[
|
|
45
|
+
{"answer_style": "verbatim"},
|
|
46
|
+
{"answer_style": ""},
|
|
47
|
+
{"no_information_reply": ""},
|
|
48
|
+
{"no_information_reply": "x" * 501},
|
|
49
|
+
{"no_information_reply": 3},
|
|
50
|
+
],
|
|
51
|
+
)
|
|
52
|
+
def test_invalid_answer_options_fail_before_the_request(options):
|
|
53
|
+
chat = MemoryChat(api_key="local-test", model="test")
|
|
54
|
+
with patch("hebbrix.chat.requests.post") as post:
|
|
55
|
+
with pytest.raises(ValueError):
|
|
56
|
+
chat.send("Where do I live?", "s", **options)
|
|
57
|
+
post.assert_not_called()
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Compact is the SDK search default (customer report 2026-10-18, issue 12)."""
|
|
2
|
+
|
|
3
|
+
from unittest.mock import AsyncMock, Mock
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from hebbrix.resources import SearchResource
|
|
7
|
+
from hebbrix.sync_client import SyncSearchResource
|
|
8
|
+
|
|
9
|
+
ENVELOPE = {
|
|
10
|
+
"query": "q",
|
|
11
|
+
"results": [{"memory_id": "m", "content": "fact", "score": 0.9}],
|
|
12
|
+
"total": 1,
|
|
13
|
+
"no_match": False,
|
|
14
|
+
"abstain_recommended": False,
|
|
15
|
+
"query_confidence": 0.9,
|
|
16
|
+
"grounding": {"status": "supported"},
|
|
17
|
+
"evidence_ids": ["m"],
|
|
18
|
+
"safety_contract_version": "search-safety-v1",
|
|
19
|
+
"view": "compact",
|
|
20
|
+
"query_entities": [],
|
|
21
|
+
"profile": {"status": "ok", "facts": [], "count": 0},
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def clients():
|
|
26
|
+
sync, asynchronous = Mock(), Mock()
|
|
27
|
+
sync.post.return_value = dict(ENVELOPE)
|
|
28
|
+
asynchronous.post = AsyncMock(return_value=dict(ENVELOPE))
|
|
29
|
+
return sync, asynchronous
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@pytest.mark.asyncio
|
|
33
|
+
async def test_search_with_proof_defaults_to_compact_and_keeps_proof():
|
|
34
|
+
sync, asynchronous = clients()
|
|
35
|
+
first = SyncSearchResource(sync).search_with_proof("q", user_id="u")
|
|
36
|
+
second = await SearchResource(asynchronous).search_with_proof("q", user_id="u")
|
|
37
|
+
assert first == second
|
|
38
|
+
assert sync.post.call_args == asynchronous.post.call_args
|
|
39
|
+
body = sync.post.call_args.kwargs["json"]
|
|
40
|
+
assert body["view"] == "compact"
|
|
41
|
+
# The proof token stays the server default for proof-bound workflows.
|
|
42
|
+
assert "include_proof_context" not in body
|
|
43
|
+
# Opt-in fields are absent unless enabled (older servers reject unknowns).
|
|
44
|
+
assert "prefer_entity_match" not in body and "include_profile" not in body
|
|
45
|
+
assert first["view"] == "compact" and first["profile"]["status"] == "ok"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@pytest.mark.asyncio
|
|
49
|
+
async def test_rows_only_search_does_not_request_the_proof_token():
|
|
50
|
+
sync, asynchronous = clients()
|
|
51
|
+
rows = SyncSearchResource(sync).search("q", user_id="u")
|
|
52
|
+
assert rows == ENVELOPE["results"]
|
|
53
|
+
await SearchResource(asynchronous).search("q", user_id="u")
|
|
54
|
+
for client in (sync, asynchronous):
|
|
55
|
+
body = client.post.call_args.kwargs["json"]
|
|
56
|
+
assert body["view"] == "compact"
|
|
57
|
+
assert body["include_proof_context"] is False
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@pytest.mark.asyncio
|
|
61
|
+
async def test_full_view_debug_and_new_options_are_explicit():
|
|
62
|
+
sync, asynchronous = clients()
|
|
63
|
+
kwargs = dict(
|
|
64
|
+
user_id="u",
|
|
65
|
+
view="full",
|
|
66
|
+
debug=True,
|
|
67
|
+
prefer_entity_match=True,
|
|
68
|
+
include_profile=True,
|
|
69
|
+
include_proof_context=True,
|
|
70
|
+
)
|
|
71
|
+
SyncSearchResource(sync).search_with_proof("q", **kwargs)
|
|
72
|
+
await SearchResource(asynchronous).search_with_proof("q", **kwargs)
|
|
73
|
+
assert sync.post.call_args == asynchronous.post.call_args
|
|
74
|
+
body = sync.post.call_args.kwargs["json"]
|
|
75
|
+
assert body["view"] == "full" and body["debug"] is True
|
|
76
|
+
assert body["prefer_entity_match"] is True and body["include_profile"] is True
|
|
77
|
+
assert body["include_proof_context"] is True
|
|
78
|
+
SyncSearchResource(sync).search("q", user_id="u", include_proof_context=True)
|
|
79
|
+
assert sync.post.call_args.kwargs["json"]["include_proof_context"] is True
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_invalid_view_still_fails_before_the_request():
|
|
83
|
+
sync, _ = clients()
|
|
84
|
+
with pytest.raises(ValueError, match="view must be full or compact"):
|
|
85
|
+
SyncSearchResource(sync).search_with_proof("q", view="tiny")
|
|
86
|
+
sync.post.assert_not_called()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
REASON = {
|
|
90
|
+
"answer": "a",
|
|
91
|
+
"sources": [{"memory_id": "m", "content": "fact", "score": 0.9}],
|
|
92
|
+
"metadata": {},
|
|
93
|
+
"no_match": False,
|
|
94
|
+
"abstain_recommended": False,
|
|
95
|
+
"query_confidence": 0.9,
|
|
96
|
+
"grounding": {"status": "supported"},
|
|
97
|
+
"evidence_ids": ["m"],
|
|
98
|
+
"safety_contract_version": "search-safety-v1",
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@pytest.mark.asyncio
|
|
103
|
+
async def test_reason_defaults_to_compact_and_debug_is_explicit():
|
|
104
|
+
sync, asynchronous = Mock(), Mock()
|
|
105
|
+
sync.post.return_value = dict(REASON)
|
|
106
|
+
asynchronous.post = AsyncMock(return_value=dict(REASON))
|
|
107
|
+
SyncSearchResource(sync).reason("q", user_id="u")
|
|
108
|
+
await SearchResource(asynchronous).reason("q", user_id="u")
|
|
109
|
+
assert sync.post.call_args == asynchronous.post.call_args
|
|
110
|
+
body = sync.post.call_args.kwargs["json"]
|
|
111
|
+
assert body["view"] == "compact"
|
|
112
|
+
assert "debug" not in body
|
|
113
|
+
SyncSearchResource(sync).reason("q", user_id="u", view="full", debug=True)
|
|
114
|
+
body = sync.post.call_args.kwargs["json"]
|
|
115
|
+
assert body["view"] == "full" and body["debug"] is True
|
|
116
|
+
with pytest.raises(ValueError):
|
|
117
|
+
SyncSearchResource(sync).reason("q", user_id="u", view="tiny")
|
|
@@ -162,6 +162,6 @@ def test_package_docs_metadata_uses_canonical_host():
|
|
|
162
162
|
assert 'Documentation = "https://hebbrix.com/docs"' in text
|
|
163
163
|
assert "docs.hebbrix.com" not in text
|
|
164
164
|
readme = (root / "README.md").read_text()
|
|
165
|
-
assert "pip install hebbrix==2.
|
|
165
|
+
assert "pip install hebbrix==2.7.0" in readme
|
|
166
166
|
assert "three calls" in readme
|
|
167
167
|
assert "schema `" not in readme
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|