hebbrix 2.6.3__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.
Files changed (49) hide show
  1. {hebbrix-2.6.3 → hebbrix-2.7.0}/CHANGELOG.md +9 -1
  2. {hebbrix-2.6.3/hebbrix.egg-info → hebbrix-2.7.0}/PKG-INFO +12 -4
  3. {hebbrix-2.6.3 → hebbrix-2.7.0}/README.md +11 -3
  4. {hebbrix-2.6.3 → hebbrix-2.7.0}/hebbrix/__init__.py +1 -1
  5. {hebbrix-2.6.3 → hebbrix-2.7.0}/hebbrix/chat.py +46 -0
  6. {hebbrix-2.6.3 → hebbrix-2.7.0}/hebbrix/client.py +7 -1
  7. {hebbrix-2.6.3 → hebbrix-2.7.0}/hebbrix/models.py +6 -0
  8. {hebbrix-2.6.3 → hebbrix-2.7.0}/hebbrix/resources.py +154 -33
  9. {hebbrix-2.6.3 → hebbrix-2.7.0}/hebbrix/sync_client.py +47 -34
  10. {hebbrix-2.6.3 → hebbrix-2.7.0/hebbrix.egg-info}/PKG-INFO +12 -4
  11. {hebbrix-2.6.3 → hebbrix-2.7.0}/hebbrix.egg-info/SOURCES.txt +2 -0
  12. {hebbrix-2.6.3 → hebbrix-2.7.0}/pyproject.toml +1 -1
  13. hebbrix-2.7.0/tests/test_chat_answer_options.py +57 -0
  14. hebbrix-2.7.0/tests/test_compact_search_default.py +117 -0
  15. {hebbrix-2.6.3 → hebbrix-2.7.0}/tests/test_customer_developer_experience.py +1 -1
  16. {hebbrix-2.6.3 → hebbrix-2.7.0}/LICENSE +0 -0
  17. {hebbrix-2.6.3 → hebbrix-2.7.0}/MANIFEST.in +0 -0
  18. {hebbrix-2.6.3 → hebbrix-2.7.0}/contracts/openapi-routes.json +0 -0
  19. {hebbrix-2.6.3 → hebbrix-2.7.0}/docs/native-experience-operations.md +0 -0
  20. {hebbrix-2.6.3 → hebbrix-2.7.0}/examples/native_reflection_worker.py +0 -0
  21. {hebbrix-2.6.3 → hebbrix-2.7.0}/hebbrix/_advice.py +0 -0
  22. {hebbrix-2.6.3 → hebbrix-2.7.0}/hebbrix/exceptions.py +0 -0
  23. {hebbrix-2.6.3 → hebbrix-2.7.0}/hebbrix/execution.py +0 -0
  24. {hebbrix-2.6.3 → hebbrix-2.7.0}/hebbrix/experience.py +0 -0
  25. {hebbrix-2.6.3 → hebbrix-2.7.0}/hebbrix/openai_reflection.py +0 -0
  26. {hebbrix-2.6.3 → hebbrix-2.7.0}/hebbrix/reflection_worker.py +0 -0
  27. {hebbrix-2.6.3 → hebbrix-2.7.0}/hebbrix/workflow.py +0 -0
  28. {hebbrix-2.6.3 → hebbrix-2.7.0}/hebbrix.egg-info/dependency_links.txt +0 -0
  29. {hebbrix-2.6.3 → hebbrix-2.7.0}/hebbrix.egg-info/entry_points.txt +0 -0
  30. {hebbrix-2.6.3 → hebbrix-2.7.0}/hebbrix.egg-info/requires.txt +0 -0
  31. {hebbrix-2.6.3 → hebbrix-2.7.0}/hebbrix.egg-info/top_level.txt +0 -0
  32. {hebbrix-2.6.3 → hebbrix-2.7.0}/setup.cfg +0 -0
  33. {hebbrix-2.6.3 → hebbrix-2.7.0}/tests/test_advanced_resources.py +0 -0
  34. {hebbrix-2.6.3 → hebbrix-2.7.0}/tests/test_advisor_integrity.py +0 -0
  35. {hebbrix-2.6.3 → hebbrix-2.7.0}/tests/test_evidence_loop_resource.py +0 -0
  36. {hebbrix-2.6.3 → hebbrix-2.7.0}/tests/test_evidence_safety_boundary.py +0 -0
  37. {hebbrix-2.6.3 → hebbrix-2.7.0}/tests/test_execution_permission.py +0 -0
  38. {hebbrix-2.6.3 → hebbrix-2.7.0}/tests/test_experience_request.py +0 -0
  39. {hebbrix-2.6.3 → hebbrix-2.7.0}/tests/test_experience_workflow.py +0 -0
  40. {hebbrix-2.6.3 → hebbrix-2.7.0}/tests/test_followup_helpers.py +0 -0
  41. {hebbrix-2.6.3 → hebbrix-2.7.0}/tests/test_memories_resource.py +0 -0
  42. {hebbrix-2.6.3 → hebbrix-2.7.0}/tests/test_openai_reflection.py +0 -0
  43. {hebbrix-2.6.3 → hebbrix-2.7.0}/tests/test_openapi_parity.py +0 -0
  44. {hebbrix-2.6.3 → hebbrix-2.7.0}/tests/test_outcome_advice_resource.py +0 -0
  45. {hebbrix-2.6.3 → hebbrix-2.7.0}/tests/test_procedural_resource.py +0 -0
  46. {hebbrix-2.6.3 → hebbrix-2.7.0}/tests/test_reflection_worker_cli.py +0 -0
  47. {hebbrix-2.6.3 → hebbrix-2.7.0}/tests/test_release_version.py +0 -0
  48. {hebbrix-2.6.3 → hebbrix-2.7.0}/tests/test_sync_client.py +0 -0
  49. {hebbrix-2.6.3 → hebbrix-2.7.0}/tests/test_workflow_route_contract.py +0 -0
@@ -1,5 +1,13 @@
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
+
3
11
  ## 2.6.3 — 2026-10-14 (candidate memory_ids)
4
12
 
5
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.
@@ -12,7 +20,7 @@
12
20
  - Add optional compact search/advice, bounded independently committed decision/outcome batches, and explicit caller-confirmation transports.
13
21
  - Add bounded chat-learning wait options while preserving the original job receipt.
14
22
  - Keep historical confidence scope aliases, ACT-as-advice and protected outcome boundaries intact.
15
- - Requires matching Round9 server routes; no benchmark superiority, latency SLA or automatic execution is implied.
23
+ - Requires a matching server release; no benchmark superiority, latency SLA or automatic execution is implied.
16
24
 
17
25
  ## 2.6.1 — 2026-10-04 (advisory horizon helpers)
18
26
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hebbrix
3
- Version: 2.6.3
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.6.3**. Start with memory storage and search, or
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.6.3
75
+ pip install hebbrix==2.7.0
76
76
  ```
77
77
 
78
78
  Python 3.8+ is supported. `MemoryClient` is asynchronous. `SyncMemoryClient`
@@ -151,13 +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
+
154
162
  Version 2.6.3 lets each `proofloop.decide` candidate carry `memory_ids`: the exact
155
163
  memories that action applies. When that candidate becomes the decision's action,
156
164
  its outcome is credited to those memories; a `proof_context` attests what was
157
165
  retrieved but never earns per-memory outcome credit by itself.
158
166
 
159
167
  Version 2.6.2 adds compact advice, batch, and explicit confirmation helpers.
160
- They require matching Round9 server routes. Compact views retain scope and safety
168
+ They require a matching server release. Compact views retain scope and safety
161
169
  caveats; complete receipts remain on the server.
162
170
 
163
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.6.3**. Start with memory storage and search, or
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.6.3
12
+ pip install hebbrix==2.7.0
13
13
  ```
14
14
 
15
15
  Python 3.8+ is supported. `MemoryClient` is asynchronous. `SyncMemoryClient`
@@ -88,13 +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
+
91
99
  Version 2.6.3 lets each `proofloop.decide` candidate carry `memory_ids`: the exact
92
100
  memories that action applies. When that candidate becomes the decision's action,
93
101
  its outcome is credited to those memories; a `proof_context` attests what was
94
102
  retrieved but never earns per-memory outcome credit by itself.
95
103
 
96
104
  Version 2.6.2 adds compact advice, batch, and explicit confirmation helpers.
97
- They require matching Round9 server routes. Compact views retain scope and safety
105
+ They require a matching server release. Compact views retain scope and safety
98
106
  caveats; complete receipts remain on the server.
99
107
 
100
108
  ## Durable readiness
@@ -5,7 +5,7 @@ production OpenAPI and ``GET /v1/users/me/capabilities``. The experimental
5
5
  World Model is intentionally absent from this release.
6
6
  """
7
7
 
8
- __version__ = "2.6.3"
8
+ __version__ = "2.7.0"
9
9
  __author__ = "Hebbrix Team"
10
10
  __license__ = "MIT"
11
11
 
@@ -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.6.3",
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] = None,
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] = None,
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={key: value for key, value in payload.items() if value is not None},
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
- "query": query,
1147
- "collection_id": collection_id,
1148
- "provider": provider,
1149
- "include_steps": include_steps,
1150
- "user_id": user_id,
1151
- "agent_id": agent_id,
1152
- "run_id": run_id,
1153
- "facets": facets or [],
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
 
@@ -1642,6 +1757,9 @@ class RLResource(BaseResource):
1642
1757
  """
1643
1758
  Train the Memory Manager agent using RL.
1644
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
+
1645
1763
  Args:
1646
1764
  num_episodes: Number of training episodes
1647
1765
  **kwargs: Additional training parameters
@@ -1665,6 +1783,9 @@ class RLResource(BaseResource):
1665
1783
  """
1666
1784
  Train the Answer Agent using RL.
1667
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
+
1668
1789
  Args:
1669
1790
  num_episodes: Number of training episodes
1670
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 _canonical_search_envelope, _memory_create_payload
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] = None,
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={key: value for key, value in payload.items() if value is not None},
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
- "query": query,
625
- "collection_id": collection_id,
626
- "provider": provider,
627
- "include_steps": include_steps,
628
- "user_id": user_id,
629
- "agent_id": agent_id,
630
- "run_id": run_id,
631
- "facets": facets or [],
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
 
@@ -1119,7 +1132,7 @@ class SyncMemoryClient:
1119
1132
  self.source = source or os.getenv("HEBBRIX_SOURCE")
1120
1133
  headers = {
1121
1134
  "Content-Type": "application/json",
1122
- "User-Agent": "hebbrix-python/2.6.3",
1135
+ "User-Agent": "hebbrix-python/2.7.0",
1123
1136
  }
1124
1137
  if api_key:
1125
1138
  headers["Authorization"] = f"Bearer {api_key}"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hebbrix
3
- Version: 2.6.3
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.6.3**. Start with memory storage and search, or
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.6.3
75
+ pip install hebbrix==2.7.0
76
76
  ```
77
77
 
78
78
  Python 3.8+ is supported. `MemoryClient` is asynchronous. `SyncMemoryClient`
@@ -151,13 +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
+
154
162
  Version 2.6.3 lets each `proofloop.decide` candidate carry `memory_ids`: the exact
155
163
  memories that action applies. When that candidate becomes the decision's action,
156
164
  its outcome is credited to those memories; a `proof_context` attests what was
157
165
  retrieved but never earns per-memory outcome credit by itself.
158
166
 
159
167
  Version 2.6.2 adds compact advice, batch, and explicit confirmation helpers.
160
- They require matching Round9 server routes. Compact views retain scope and safety
168
+ They require a matching server release. Compact views retain scope and safety
161
169
  caveats; complete receipts remain on the server.
162
170
 
163
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "hebbrix"
7
- version = "2.6.3"
7
+ version = "2.7.0"
8
8
  description = "Typed Python client for Hebbrix memory, retrieval, and outcome-learning APIs"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -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.6.3" in readme
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