awareness-memory-cloud 2.4.4__tar.gz → 2.6.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.
- {awareness_memory_cloud-2.4.4 → awareness_memory_cloud-2.6.0}/PKG-INFO +15 -1
- {awareness_memory_cloud-2.4.4 → awareness_memory_cloud-2.6.0}/README.md +14 -0
- {awareness_memory_cloud-2.4.4 → awareness_memory_cloud-2.6.0}/awareness_memory_cloud.egg-info/PKG-INFO +15 -1
- {awareness_memory_cloud-2.4.4 → awareness_memory_cloud-2.6.0}/awareness_memory_cloud.egg-info/SOURCES.txt +3 -1
- {awareness_memory_cloud-2.4.4 → awareness_memory_cloud-2.6.0}/memory_cloud/client.py +82 -7
- {awareness_memory_cloud-2.4.4 → awareness_memory_cloud-2.6.0}/memory_cloud/types.py +11 -2
- {awareness_memory_cloud-2.4.4 → awareness_memory_cloud-2.6.0}/pyproject.toml +1 -1
- {awareness_memory_cloud-2.4.4 → awareness_memory_cloud-2.6.0}/tests/test_export_reader.py +5 -2
- awareness_memory_cloud-2.6.0/tests/test_hyde.py +200 -0
- awareness_memory_cloud-2.6.0/tests/test_skill_types.py +86 -0
- {awareness_memory_cloud-2.4.4 → awareness_memory_cloud-2.6.0}/LICENSE +0 -0
- {awareness_memory_cloud-2.4.4 → awareness_memory_cloud-2.6.0}/awareness_memory_cloud.egg-info/dependency_links.txt +0 -0
- {awareness_memory_cloud-2.4.4 → awareness_memory_cloud-2.6.0}/awareness_memory_cloud.egg-info/requires.txt +0 -0
- {awareness_memory_cloud-2.4.4 → awareness_memory_cloud-2.6.0}/awareness_memory_cloud.egg-info/top_level.txt +0 -0
- {awareness_memory_cloud-2.4.4 → awareness_memory_cloud-2.6.0}/memory_cloud/__init__.py +0 -0
- {awareness_memory_cloud-2.4.4 → awareness_memory_cloud-2.6.0}/memory_cloud/errors.py +0 -0
- {awareness_memory_cloud-2.4.4 → awareness_memory_cloud-2.6.0}/memory_cloud/export_reader.py +0 -0
- {awareness_memory_cloud-2.4.4 → awareness_memory_cloud-2.6.0}/memory_cloud/integrations/__init__.py +0 -0
- {awareness_memory_cloud-2.4.4 → awareness_memory_cloud-2.6.0}/memory_cloud/integrations/_base.py +0 -0
- {awareness_memory_cloud-2.4.4 → awareness_memory_cloud-2.6.0}/memory_cloud/integrations/autogen.py +0 -0
- {awareness_memory_cloud-2.4.4 → awareness_memory_cloud-2.6.0}/memory_cloud/integrations/crewai.py +0 -0
- {awareness_memory_cloud-2.4.4 → awareness_memory_cloud-2.6.0}/memory_cloud/integrations/langchain.py +0 -0
- {awareness_memory_cloud-2.4.4 → awareness_memory_cloud-2.6.0}/memory_cloud/integrations/praisonai.py +0 -0
- {awareness_memory_cloud-2.4.4 → awareness_memory_cloud-2.6.0}/memory_cloud/interceptor.py +0 -0
- {awareness_memory_cloud-2.4.4 → awareness_memory_cloud-2.6.0}/memory_cloud/query_rewrite.py +0 -0
- {awareness_memory_cloud-2.4.4 → awareness_memory_cloud-2.6.0}/memory_cloud/quickstart.py +0 -0
- {awareness_memory_cloud-2.4.4 → awareness_memory_cloud-2.6.0}/setup.cfg +0 -0
- {awareness_memory_cloud-2.4.4 → awareness_memory_cloud-2.6.0}/tests/test_client_chat_and_record.py +0 -0
- {awareness_memory_cloud-2.4.4 → awareness_memory_cloud-2.6.0}/tests/test_client_crud.py +0 -0
- {awareness_memory_cloud-2.4.4 → awareness_memory_cloud-2.6.0}/tests/test_client_extraction_passthrough.py +0 -0
- {awareness_memory_cloud-2.4.4 → awareness_memory_cloud-2.6.0}/tests/test_client_recall_and_compaction.py +0 -0
- {awareness_memory_cloud-2.4.4 → awareness_memory_cloud-2.6.0}/tests/test_client_tasks_agents_users.py +0 -0
- {awareness_memory_cloud-2.4.4 → awareness_memory_cloud-2.6.0}/tests/test_client_upload_export_keys.py +0 -0
- {awareness_memory_cloud-2.4.4 → awareness_memory_cloud-2.6.0}/tests/test_ecosystem_smoke.py +0 -0
- {awareness_memory_cloud-2.4.4 → awareness_memory_cloud-2.6.0}/tests/test_integrations.py +0 -0
- {awareness_memory_cloud-2.4.4 → awareness_memory_cloud-2.6.0}/tests/test_interceptor_response_format_fallback.py +0 -0
- {awareness_memory_cloud-2.4.4 → awareness_memory_cloud-2.6.0}/tests/test_interceptor_wrap.py +0 -0
- {awareness_memory_cloud-2.4.4 → awareness_memory_cloud-2.6.0}/tests/test_quickstart.py +0 -0
- {awareness_memory_cloud-2.4.4 → awareness_memory_cloud-2.6.0}/tests/test_sdk_functional.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: awareness-memory-cloud
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.6.0
|
|
4
4
|
Summary: Python SDK for Awareness Memory Cloud
|
|
5
5
|
Author: Awareness Team
|
|
6
6
|
License: Apache License
|
|
@@ -638,3 +638,17 @@ export AWARENESS_OWNER_ID="your-owner-id"
|
|
|
638
638
|
|
|
639
639
|
python examples/e2e_langchain_cloud.py
|
|
640
640
|
```
|
|
641
|
+
|
|
642
|
+
---
|
|
643
|
+
|
|
644
|
+
## What makes Awareness different
|
|
645
|
+
|
|
646
|
+
Most memory systems pick one extraction strategy. Awareness combines them:
|
|
647
|
+
|
|
648
|
+
- **Hybrid retrieval by default** — BM25 full-text + vector cosine + knowledge-graph 1-hop expansion, fused with Reciprocal Rank Fusion. 95.6% R@5 on LongMemEval, zero LLM calls on the retrieval side.
|
|
649
|
+
- **Salience-aware extraction** — the client's own LLM self-scores every card on `novelty` / `durability` / `specificity`; cards below 0.4 on novelty or durability are dropped server-side. Framework metadata (`Sender (untrusted metadata)`, `turn_brief`) is filtered before extraction runs.
|
|
650
|
+
- **Project isolation** — `X-Awareness-Project-Dir` header scopes memory per project.
|
|
651
|
+
- **Zero-LLM backend** — all extraction runs on your LLM (Claude, GPT-4, Gemini, local Llama). The backend is a coordinator + storage layer; no inference costs pass through to you.
|
|
652
|
+
- **One memory, many clients** — same data reachable via Claude Code, OpenClaw, npm / pip / ClawHub, MCP server.
|
|
653
|
+
|
|
654
|
+
See [`docs/analysis/MEMPALACE_COMPARISON_2026-04-17.md`](https://github.com/edwin-hao-ai/Awareness/blob/main/docs/analysis/MEMPALACE_COMPARISON_2026-04-17.md) for the honest side-by-side against MemPalace.
|
|
@@ -389,3 +389,17 @@ export AWARENESS_OWNER_ID="your-owner-id"
|
|
|
389
389
|
|
|
390
390
|
python examples/e2e_langchain_cloud.py
|
|
391
391
|
```
|
|
392
|
+
|
|
393
|
+
---
|
|
394
|
+
|
|
395
|
+
## What makes Awareness different
|
|
396
|
+
|
|
397
|
+
Most memory systems pick one extraction strategy. Awareness combines them:
|
|
398
|
+
|
|
399
|
+
- **Hybrid retrieval by default** — BM25 full-text + vector cosine + knowledge-graph 1-hop expansion, fused with Reciprocal Rank Fusion. 95.6% R@5 on LongMemEval, zero LLM calls on the retrieval side.
|
|
400
|
+
- **Salience-aware extraction** — the client's own LLM self-scores every card on `novelty` / `durability` / `specificity`; cards below 0.4 on novelty or durability are dropped server-side. Framework metadata (`Sender (untrusted metadata)`, `turn_brief`) is filtered before extraction runs.
|
|
401
|
+
- **Project isolation** — `X-Awareness-Project-Dir` header scopes memory per project.
|
|
402
|
+
- **Zero-LLM backend** — all extraction runs on your LLM (Claude, GPT-4, Gemini, local Llama). The backend is a coordinator + storage layer; no inference costs pass through to you.
|
|
403
|
+
- **One memory, many clients** — same data reachable via Claude Code, OpenClaw, npm / pip / ClawHub, MCP server.
|
|
404
|
+
|
|
405
|
+
See [`docs/analysis/MEMPALACE_COMPARISON_2026-04-17.md`](https://github.com/edwin-hao-ai/Awareness/blob/main/docs/analysis/MEMPALACE_COMPARISON_2026-04-17.md) for the honest side-by-side against MemPalace.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: awareness-memory-cloud
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.6.0
|
|
4
4
|
Summary: Python SDK for Awareness Memory Cloud
|
|
5
5
|
Author: Awareness Team
|
|
6
6
|
License: Apache License
|
|
@@ -638,3 +638,17 @@ export AWARENESS_OWNER_ID="your-owner-id"
|
|
|
638
638
|
|
|
639
639
|
python examples/e2e_langchain_cloud.py
|
|
640
640
|
```
|
|
641
|
+
|
|
642
|
+
---
|
|
643
|
+
|
|
644
|
+
## What makes Awareness different
|
|
645
|
+
|
|
646
|
+
Most memory systems pick one extraction strategy. Awareness combines them:
|
|
647
|
+
|
|
648
|
+
- **Hybrid retrieval by default** — BM25 full-text + vector cosine + knowledge-graph 1-hop expansion, fused with Reciprocal Rank Fusion. 95.6% R@5 on LongMemEval, zero LLM calls on the retrieval side.
|
|
649
|
+
- **Salience-aware extraction** — the client's own LLM self-scores every card on `novelty` / `durability` / `specificity`; cards below 0.4 on novelty or durability are dropped server-side. Framework metadata (`Sender (untrusted metadata)`, `turn_brief`) is filtered before extraction runs.
|
|
650
|
+
- **Project isolation** — `X-Awareness-Project-Dir` header scopes memory per project.
|
|
651
|
+
- **Zero-LLM backend** — all extraction runs on your LLM (Claude, GPT-4, Gemini, local Llama). The backend is a coordinator + storage layer; no inference costs pass through to you.
|
|
652
|
+
- **One memory, many clients** — same data reachable via Claude Code, OpenClaw, npm / pip / ClawHub, MCP server.
|
|
653
|
+
|
|
654
|
+
See [`docs/analysis/MEMPALACE_COMPARISON_2026-04-17.md`](https://github.com/edwin-hao-ai/Awareness/blob/main/docs/analysis/MEMPALACE_COMPARISON_2026-04-17.md) for the honest side-by-side against MemPalace.
|
|
@@ -28,8 +28,10 @@ tests/test_client_tasks_agents_users.py
|
|
|
28
28
|
tests/test_client_upload_export_keys.py
|
|
29
29
|
tests/test_ecosystem_smoke.py
|
|
30
30
|
tests/test_export_reader.py
|
|
31
|
+
tests/test_hyde.py
|
|
31
32
|
tests/test_integrations.py
|
|
32
33
|
tests/test_interceptor_response_format_fallback.py
|
|
33
34
|
tests/test_interceptor_wrap.py
|
|
34
35
|
tests/test_quickstart.py
|
|
35
|
-
tests/test_sdk_functional.py
|
|
36
|
+
tests/test_sdk_functional.py
|
|
37
|
+
tests/test_skill_types.py
|
|
@@ -333,6 +333,7 @@ class MemoryCloudClient:
|
|
|
333
333
|
include_installed: bool = True,
|
|
334
334
|
detail: Optional[str] = None,
|
|
335
335
|
ids: Optional[List[str]] = None,
|
|
336
|
+
hyde_hint: Optional[str] = None,
|
|
336
337
|
trace_id: Optional[str] = None,
|
|
337
338
|
) -> Dict[str, Any]:
|
|
338
339
|
"""Retrieve from memory using the specified recall mode.
|
|
@@ -419,24 +420,42 @@ class MemoryCloudClient:
|
|
|
419
420
|
body["detail"] = detail
|
|
420
421
|
if ids is not None:
|
|
421
422
|
body["ids"] = ids
|
|
423
|
+
if hyde_hint and hyde_hint.strip():
|
|
424
|
+
body["hyde_hint"] = hyde_hint.strip()
|
|
422
425
|
|
|
423
426
|
# Local daemon bridge: route through awareness_recall MCP tool.
|
|
427
|
+
# F-053 single-parameter surface — pass only `query` and let the daemon
|
|
428
|
+
# auto-route (query-type classifier + recency channel + budget-tier
|
|
429
|
+
# shaping). Legacy fields are still forwarded for backwards-compat with
|
|
430
|
+
# pre-F-053 daemons but marked explicitly so server-side deprecation
|
|
431
|
+
# logging can trace who's still sending them.
|
|
424
432
|
if self._should_use_daemon():
|
|
425
433
|
daemon_args: Dict[str, Any] = {
|
|
426
|
-
"
|
|
434
|
+
"query": query,
|
|
427
435
|
"limit": limit,
|
|
428
|
-
"detail": detail or "summary",
|
|
429
436
|
}
|
|
430
|
-
|
|
437
|
+
# Token budget drives bucket shaping (raw-heavy / mixed / card-only).
|
|
438
|
+
# Callers can override via custom_kwargs["token_budget"].
|
|
439
|
+
token_budget = (custom_kwargs or {}).get("token_budget")
|
|
440
|
+
if token_budget is not None:
|
|
441
|
+
daemon_args["token_budget"] = int(token_budget)
|
|
442
|
+
if agent_role:
|
|
443
|
+
daemon_args["agent_role"] = agent_role
|
|
444
|
+
# Legacy fields — only forwarded if caller explicitly passed them.
|
|
445
|
+
# Daemon will log [deprecated param used] for these.
|
|
446
|
+
if effective_keyword and keyword_query is not None:
|
|
431
447
|
daemon_args["keyword_query"] = effective_keyword
|
|
432
448
|
if scope:
|
|
433
449
|
daemon_args["scope"] = scope
|
|
434
|
-
if recall_mode:
|
|
450
|
+
if recall_mode and recall_mode != "precise":
|
|
451
|
+
# "precise" is the SDK default — don't forward it as legacy.
|
|
435
452
|
daemon_args["recall_mode"] = recall_mode
|
|
436
|
-
if
|
|
437
|
-
daemon_args["
|
|
453
|
+
if detail is not None:
|
|
454
|
+
daemon_args["detail"] = detail
|
|
438
455
|
if ids:
|
|
439
456
|
daemon_args["ids"] = ids
|
|
457
|
+
if hyde_hint and hyde_hint.strip():
|
|
458
|
+
daemon_args["hyde_hint"] = hyde_hint.strip()
|
|
440
459
|
daemon_result = self.call_local_daemon("awareness_recall", daemon_args)
|
|
441
460
|
items = daemon_result.get("items") or daemon_result.get("results") or []
|
|
442
461
|
return {"results": items}
|
|
@@ -449,6 +468,50 @@ class MemoryCloudClient:
|
|
|
449
468
|
)
|
|
450
469
|
return self._attach_trace(payload, resolved_trace_id)
|
|
451
470
|
|
|
471
|
+
def retrieve_with_hyde(
|
|
472
|
+
self,
|
|
473
|
+
memory_id: str,
|
|
474
|
+
query: str,
|
|
475
|
+
llm_complete: Callable[[str], str],
|
|
476
|
+
**kwargs: Any,
|
|
477
|
+
) -> Dict[str, Any]:
|
|
478
|
+
"""Retrieve with a user-supplied LLM generating a HyDE
|
|
479
|
+
(Hypothetical Document Embeddings) hint.
|
|
480
|
+
|
|
481
|
+
The caller passes a ``llm_complete`` callable that wraps their own
|
|
482
|
+
LLM provider (OpenAI, Anthropic, ollama, etc.). If the LLM call
|
|
483
|
+
fails or returns a too-short string, this method falls back to a
|
|
484
|
+
plain ``retrieve()`` with no ``hyde_hint`` — HyDE is always an
|
|
485
|
+
optional boost, never required.
|
|
486
|
+
|
|
487
|
+
:param memory_id: Target memory id.
|
|
488
|
+
:param query: The raw user query.
|
|
489
|
+
:param llm_complete: Sync callable taking a prompt string and
|
|
490
|
+
returning a completion string. Any raised exception is
|
|
491
|
+
swallowed and causes fallback to plain retrieve.
|
|
492
|
+
:param kwargs: Forwarded verbatim to ``retrieve()`` (e.g.
|
|
493
|
+
``limit``, ``recall_mode``, ``user_id`` …). Do not pass
|
|
494
|
+
``hyde_hint`` here — it is generated by this helper.
|
|
495
|
+
"""
|
|
496
|
+
hyde_prompt = (
|
|
497
|
+
"Write a single paragraph (100-200 characters) hypothetical "
|
|
498
|
+
"answer to this question, as if it were already in a knowledge "
|
|
499
|
+
"base. No preamble, no markdown.\n\n"
|
|
500
|
+
f"Question: {query}"
|
|
501
|
+
)
|
|
502
|
+
hyde_hint: Optional[str] = None
|
|
503
|
+
try:
|
|
504
|
+
generated = llm_complete(hyde_prompt)
|
|
505
|
+
if generated and len(generated.strip()) >= 20:
|
|
506
|
+
hyde_hint = generated.strip()[:400]
|
|
507
|
+
except Exception:
|
|
508
|
+
# Any LLM error → fall back to raw retrieve (no hyde_hint)
|
|
509
|
+
pass
|
|
510
|
+
# Strip a caller-supplied hyde_hint to avoid duplicate-kwarg TypeError
|
|
511
|
+
# if they passed one accidentally; the helper's generated value wins.
|
|
512
|
+
kwargs.pop("hyde_hint", None)
|
|
513
|
+
return self.retrieve(memory_id, query, hyde_hint=hyde_hint, **kwargs)
|
|
514
|
+
|
|
452
515
|
def write(
|
|
453
516
|
self,
|
|
454
517
|
memory_id: str,
|
|
@@ -690,6 +753,10 @@ class MemoryCloudClient:
|
|
|
690
753
|
result["events_sent"] = len(capped)
|
|
691
754
|
if ingest_result.get("trace_id"):
|
|
692
755
|
result["trace_id"] = ingest_result["trace_id"]
|
|
756
|
+
# Hoist extraction_request to top-level so callers can see that
|
|
757
|
+
# extraction was scheduled without reaching into result["ingest"].
|
|
758
|
+
if ingest_result.get("extraction_request") is not None:
|
|
759
|
+
result["extraction_request"] = ingest_result["extraction_request"]
|
|
693
760
|
|
|
694
761
|
# --- Submit insights directly ---
|
|
695
762
|
if insights is not None:
|
|
@@ -1521,22 +1588,30 @@ class MemoryCloudClient:
|
|
|
1521
1588
|
self,
|
|
1522
1589
|
memory_id: str,
|
|
1523
1590
|
skill_id: str,
|
|
1591
|
+
outcome: Optional[str] = "success",
|
|
1524
1592
|
trace_id: Optional[str] = None,
|
|
1525
1593
|
) -> Dict[str, Any]:
|
|
1526
1594
|
"""
|
|
1527
|
-
Mark a skill as used
|
|
1595
|
+
Mark a skill as used with outcome feedback.
|
|
1528
1596
|
|
|
1529
1597
|
Args:
|
|
1530
1598
|
memory_id: Target memory id.
|
|
1531
1599
|
skill_id: The skill to mark as used.
|
|
1600
|
+
outcome: "success" (default), "partial", or "failed".
|
|
1601
|
+
Failed outcomes decrease confidence; 3+ consecutive
|
|
1602
|
+
failures auto-flag the skill for review.
|
|
1532
1603
|
trace_id: Optional trace id.
|
|
1533
1604
|
|
|
1534
1605
|
Returns:
|
|
1535
1606
|
Updated skill data.
|
|
1536
1607
|
"""
|
|
1608
|
+
body = None
|
|
1609
|
+
if outcome and outcome != "success":
|
|
1610
|
+
body = {"outcome": outcome}
|
|
1537
1611
|
data, resolved_trace = self._request(
|
|
1538
1612
|
method="POST",
|
|
1539
1613
|
path=f"/memories/{memory_id}/skills/{skill_id}/use",
|
|
1614
|
+
json_payload=body,
|
|
1540
1615
|
trace_id=trace_id,
|
|
1541
1616
|
)
|
|
1542
1617
|
return self._attach_trace(data, resolved_trace)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Any, Dict, List, Optional, TypedDict
|
|
1
|
+
from typing import Any, Dict, List, Literal, Optional, TypedDict
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
class RetrieveResult(TypedDict, total=False):
|
|
@@ -423,7 +423,12 @@ class Skill(TypedDict, total=False):
|
|
|
423
423
|
trigger_conditions: List[SkillTrigger]
|
|
424
424
|
tags: List[str]
|
|
425
425
|
source_card_ids: List[str]
|
|
426
|
-
|
|
426
|
+
# Lifecycle stage. Kept as plain str for legacy rows, but the typed union
|
|
427
|
+
# is exposed via `GrowthStage` for call sites that want narrower typing.
|
|
428
|
+
growth_stage: Literal["seedling", "budding", "evergreen"]
|
|
429
|
+
# F-059 — known failure modes + post-run checks. Optional for back-compat.
|
|
430
|
+
pitfalls: List[str]
|
|
431
|
+
verification: List[str]
|
|
427
432
|
usage_count: int
|
|
428
433
|
decay_score: float
|
|
429
434
|
pinned: bool
|
|
@@ -434,6 +439,10 @@ class Skill(TypedDict, total=False):
|
|
|
434
439
|
last_used_at: str
|
|
435
440
|
|
|
436
441
|
|
|
442
|
+
#: Exposed for consumers that want to type-narrow growth_stage.
|
|
443
|
+
GrowthStage = Literal["seedling", "budding", "evergreen"]
|
|
444
|
+
|
|
445
|
+
|
|
437
446
|
class SkillListResult(TypedDict, total=False):
|
|
438
447
|
skills: List[Skill]
|
|
439
448
|
total: int
|
|
@@ -40,10 +40,13 @@ def test_mcp_helper_session_and_history_parse():
|
|
|
40
40
|
second = client._begin_memory_session(memory_id="m1", source="python-sdk")
|
|
41
41
|
assert first["session_id"] != second["session_id"]
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
# v2.0.0 removed _coerce_history_to_events; equivalent behaviour is now
|
|
44
|
+
# `_build_record_events` accepting a list of transcript lines.
|
|
45
|
+
backfilled = client._build_record_events(
|
|
46
|
+
["User: hi", "Assistant: done"],
|
|
45
47
|
source="python-sdk",
|
|
46
48
|
session_id=second["session_id"],
|
|
49
|
+
scope="timeline",
|
|
47
50
|
)
|
|
48
51
|
assert len(backfilled) == 2
|
|
49
52
|
assert backfilled[0]["session_id"] == second["session_id"]
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""Tests for ``retrieve_with_hyde`` helper on the Python SDK.
|
|
2
|
+
|
|
3
|
+
Covers the four behaviours we care about:
|
|
4
|
+
1. Happy path: LLM returns a sensible hypothetical answer → it is
|
|
5
|
+
trimmed, clamped to 400 chars, and forwarded to ``retrieve`` as the
|
|
6
|
+
``hyde_hint`` kwarg.
|
|
7
|
+
2. Fallback: ``llm_complete`` raises → the helper swallows the error
|
|
8
|
+
and calls ``retrieve`` with ``hyde_hint=None``.
|
|
9
|
+
3. Too-short output (<20 chars after strip) is dropped.
|
|
10
|
+
4. Plain ``retrieve(hyde_hint="…")`` wires the value into the POST
|
|
11
|
+
body so the new backend parameter is actually on the wire.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from typing import Any, Dict
|
|
17
|
+
|
|
18
|
+
import pytest
|
|
19
|
+
|
|
20
|
+
from memory_cloud.client import MemoryCloudClient
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _make_client() -> MemoryCloudClient:
|
|
24
|
+
return MemoryCloudClient(
|
|
25
|
+
base_url="http://localhost:8000/api/v1",
|
|
26
|
+
api_key="test-key",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_retrieve_with_hyde_forwards_generated_hint() -> None:
|
|
31
|
+
client = _make_client()
|
|
32
|
+
|
|
33
|
+
captured: Dict[str, Any] = {}
|
|
34
|
+
prompt_seen: Dict[str, str] = {}
|
|
35
|
+
|
|
36
|
+
def llm_complete(prompt: str) -> str:
|
|
37
|
+
prompt_seen["value"] = prompt
|
|
38
|
+
return (
|
|
39
|
+
"JWT refresh tokens live for 30 days, rotate on every use, "
|
|
40
|
+
"and are stored hashed in users.refresh_token_hash."
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def fake_retrieve(*args: Any, **kwargs: Any) -> Dict[str, Any]:
|
|
44
|
+
# Support either positional or keyword passthrough
|
|
45
|
+
if args:
|
|
46
|
+
kwargs.setdefault("memory_id", args[0] if len(args) > 0 else None)
|
|
47
|
+
kwargs.setdefault("query", args[1] if len(args) > 1 else None)
|
|
48
|
+
captured.update(kwargs)
|
|
49
|
+
return {"results": [{"content": "hit"}]}
|
|
50
|
+
|
|
51
|
+
client.retrieve = fake_retrieve # type: ignore[method-assign]
|
|
52
|
+
|
|
53
|
+
out = client.retrieve_with_hyde(
|
|
54
|
+
"memory-1",
|
|
55
|
+
"How long do refresh tokens live?",
|
|
56
|
+
llm_complete,
|
|
57
|
+
limit=5,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
assert out["results"] == [{"content": "hit"}]
|
|
61
|
+
assert captured["memory_id"] == "memory-1"
|
|
62
|
+
assert captured["query"] == "How long do refresh tokens live?"
|
|
63
|
+
assert captured["limit"] == 5
|
|
64
|
+
assert isinstance(captured["hyde_hint"], str)
|
|
65
|
+
assert captured["hyde_hint"].startswith("JWT refresh tokens live")
|
|
66
|
+
assert "hypothetical answer" in prompt_seen["value"]
|
|
67
|
+
assert "How long do refresh tokens live?" in prompt_seen["value"]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_retrieve_with_hyde_falls_back_when_llm_raises() -> None:
|
|
71
|
+
client = _make_client()
|
|
72
|
+
captured: Dict[str, Any] = {}
|
|
73
|
+
|
|
74
|
+
def llm_complete(_prompt: str) -> str:
|
|
75
|
+
raise RuntimeError("LLM provider down")
|
|
76
|
+
|
|
77
|
+
def fake_retrieve(*args: Any, **kwargs: Any) -> Dict[str, Any]:
|
|
78
|
+
if args:
|
|
79
|
+
kwargs.setdefault("memory_id", args[0] if len(args) > 0 else None)
|
|
80
|
+
kwargs.setdefault("query", args[1] if len(args) > 1 else None)
|
|
81
|
+
captured.update(kwargs)
|
|
82
|
+
return {"results": []}
|
|
83
|
+
|
|
84
|
+
client.retrieve = fake_retrieve # type: ignore[method-assign]
|
|
85
|
+
|
|
86
|
+
client.retrieve_with_hyde(
|
|
87
|
+
"memory-2",
|
|
88
|
+
"fallback query",
|
|
89
|
+
llm_complete,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
assert captured["query"] == "fallback query"
|
|
93
|
+
assert captured["hyde_hint"] is None, (
|
|
94
|
+
"When the user's LLM raises, we must fall back to plain retrieve "
|
|
95
|
+
"with hyde_hint=None, not crash or silently send a partial hint."
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_retrieve_with_hyde_drops_too_short_output() -> None:
|
|
100
|
+
client = _make_client()
|
|
101
|
+
captured: Dict[str, Any] = {}
|
|
102
|
+
|
|
103
|
+
def llm_complete(_prompt: str) -> str:
|
|
104
|
+
return "too short" # 9 chars < 20 threshold
|
|
105
|
+
|
|
106
|
+
def fake_retrieve(*args: Any, **kwargs: Any) -> Dict[str, Any]:
|
|
107
|
+
if args:
|
|
108
|
+
kwargs.setdefault("memory_id", args[0] if len(args) > 0 else None)
|
|
109
|
+
kwargs.setdefault("query", args[1] if len(args) > 1 else None)
|
|
110
|
+
captured.update(kwargs)
|
|
111
|
+
return {"results": []}
|
|
112
|
+
|
|
113
|
+
client.retrieve = fake_retrieve # type: ignore[method-assign]
|
|
114
|
+
|
|
115
|
+
client.retrieve_with_hyde("memory-3", "short-output q", llm_complete)
|
|
116
|
+
|
|
117
|
+
assert captured["hyde_hint"] is None
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_retrieve_with_hyde_clamps_long_output() -> None:
|
|
121
|
+
client = _make_client()
|
|
122
|
+
captured: Dict[str, Any] = {}
|
|
123
|
+
|
|
124
|
+
def llm_complete(_prompt: str) -> str:
|
|
125
|
+
return "A" * 1200
|
|
126
|
+
|
|
127
|
+
def fake_retrieve(*args: Any, **kwargs: Any) -> Dict[str, Any]:
|
|
128
|
+
if args:
|
|
129
|
+
kwargs.setdefault("memory_id", args[0] if len(args) > 0 else None)
|
|
130
|
+
kwargs.setdefault("query", args[1] if len(args) > 1 else None)
|
|
131
|
+
captured.update(kwargs)
|
|
132
|
+
return {"results": []}
|
|
133
|
+
|
|
134
|
+
client.retrieve = fake_retrieve # type: ignore[method-assign]
|
|
135
|
+
|
|
136
|
+
client.retrieve_with_hyde("memory-4", "clamp check", llm_complete)
|
|
137
|
+
|
|
138
|
+
assert isinstance(captured["hyde_hint"], str)
|
|
139
|
+
assert len(captured["hyde_hint"]) == 400
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_retrieve_wires_hyde_hint_into_post_body() -> None:
|
|
143
|
+
"""Direct retrieve(hyde_hint=…) must forward the field on the wire."""
|
|
144
|
+
client = _make_client()
|
|
145
|
+
captured: Dict[str, Any] = {}
|
|
146
|
+
|
|
147
|
+
def fake_request(
|
|
148
|
+
*,
|
|
149
|
+
method: str,
|
|
150
|
+
path: str,
|
|
151
|
+
json_payload: Any = None,
|
|
152
|
+
trace_id: Any = None,
|
|
153
|
+
**_: Any,
|
|
154
|
+
):
|
|
155
|
+
captured["method"] = method
|
|
156
|
+
captured["path"] = path
|
|
157
|
+
captured["body"] = json_payload
|
|
158
|
+
return ({"results": []}, trace_id or "trace-x")
|
|
159
|
+
|
|
160
|
+
client._request = fake_request # type: ignore[method-assign]
|
|
161
|
+
|
|
162
|
+
client.retrieve(
|
|
163
|
+
"memory-5",
|
|
164
|
+
"explicit hint",
|
|
165
|
+
hyde_hint=" A pre-computed hypothetical passage for the query. ",
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
assert captured["method"] == "POST"
|
|
169
|
+
assert captured["path"] == "/memories/memory-5/retrieve"
|
|
170
|
+
assert (
|
|
171
|
+
captured["body"]["hyde_hint"]
|
|
172
|
+
== "A pre-computed hypothetical passage for the query."
|
|
173
|
+
), "Explicit hyde_hint must be trimmed and sent on the wire"
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def test_retrieve_omits_hyde_hint_when_not_set() -> None:
|
|
177
|
+
"""Backward compat: callers who do not pass hyde_hint see no new field."""
|
|
178
|
+
client = _make_client()
|
|
179
|
+
captured: Dict[str, Any] = {}
|
|
180
|
+
|
|
181
|
+
def fake_request(
|
|
182
|
+
*,
|
|
183
|
+
method: str,
|
|
184
|
+
path: str,
|
|
185
|
+
json_payload: Any = None,
|
|
186
|
+
trace_id: Any = None,
|
|
187
|
+
**_: Any,
|
|
188
|
+
):
|
|
189
|
+
captured["body"] = json_payload
|
|
190
|
+
return ({"results": []}, trace_id or "trace-y")
|
|
191
|
+
|
|
192
|
+
client._request = fake_request # type: ignore[method-assign]
|
|
193
|
+
|
|
194
|
+
client.retrieve("memory-6", "no hint here")
|
|
195
|
+
|
|
196
|
+
assert "hyde_hint" not in captured["body"]
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
if __name__ == "__main__": # pragma: no cover
|
|
200
|
+
pytest.main([__file__, "-v"])
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Unit tests for `Skill` TypedDict shape (F-059 additions).
|
|
2
|
+
|
|
3
|
+
Ensures that `pitfalls`, `verification`, and `growth_stage` can be populated
|
|
4
|
+
without tripping type errors and that the TypedDict can round-trip through
|
|
5
|
+
JSON serialization unchanged — the SDK treats these as passthrough fields
|
|
6
|
+
between the backend/daemon and the caller.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
from memory_cloud.types import GrowthStage, Skill
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _base_skill() -> Skill:
|
|
17
|
+
return Skill(
|
|
18
|
+
id="skill-1",
|
|
19
|
+
memory_id="mem-1",
|
|
20
|
+
name="Deploy with Docker Compose",
|
|
21
|
+
summary="Rebuild backend image and restart dependent services.",
|
|
22
|
+
methods=[{"step": 1, "description": "docker compose build backend"}],
|
|
23
|
+
trigger_conditions=[{"pattern": "deploy", "weight": 1.0}],
|
|
24
|
+
tags=["deployment"],
|
|
25
|
+
source_card_ids=["card-a", "card-b"],
|
|
26
|
+
growth_stage="budding",
|
|
27
|
+
usage_count=3,
|
|
28
|
+
decay_score=0.72,
|
|
29
|
+
pinned=False,
|
|
30
|
+
status="active",
|
|
31
|
+
created_at="2026-04-19T00:00:00Z",
|
|
32
|
+
updated_at="2026-04-19T00:00:00Z",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@pytest.mark.unit
|
|
37
|
+
def test_skill_accepts_f059_optional_fields() -> None:
|
|
38
|
+
skill: Skill = _base_skill()
|
|
39
|
+
skill["pitfalls"] = [
|
|
40
|
+
"Do not rebuild postgres — resets scram-sha-256 hash",
|
|
41
|
+
"Avoid `docker compose up` without --env-file .env.prod",
|
|
42
|
+
]
|
|
43
|
+
skill["verification"] = [
|
|
44
|
+
"curl https://awareness.market/health returns 200",
|
|
45
|
+
"docker compose ps shows all services healthy",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
assert skill["pitfalls"][0].startswith("Do not rebuild")
|
|
49
|
+
assert len(skill["verification"]) == 2
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@pytest.mark.unit
|
|
53
|
+
def test_skill_round_trips_through_json_with_new_fields() -> None:
|
|
54
|
+
skill: Skill = _base_skill()
|
|
55
|
+
skill["pitfalls"] = ["p1", "p2"]
|
|
56
|
+
skill["verification"] = ["v1"]
|
|
57
|
+
|
|
58
|
+
encoded = json.dumps(skill)
|
|
59
|
+
decoded = json.loads(encoded)
|
|
60
|
+
|
|
61
|
+
assert decoded["pitfalls"] == ["p1", "p2"]
|
|
62
|
+
assert decoded["verification"] == ["v1"]
|
|
63
|
+
assert decoded["growth_stage"] == "budding"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@pytest.mark.unit
|
|
67
|
+
def test_growth_stage_union_accepts_valid_values() -> None:
|
|
68
|
+
# Literal values — these should all be accepted by type-checkers.
|
|
69
|
+
for stage in ("seedling", "budding", "evergreen"):
|
|
70
|
+
value: GrowthStage = stage # type: ignore[assignment]
|
|
71
|
+
skill: Skill = _base_skill()
|
|
72
|
+
skill["growth_stage"] = value
|
|
73
|
+
assert skill["growth_stage"] == stage
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@pytest.mark.unit
|
|
77
|
+
def test_skill_without_f059_fields_is_still_valid() -> None:
|
|
78
|
+
"""Back-compat: skills created before F-059 must still parse cleanly."""
|
|
79
|
+
skill: Skill = _base_skill()
|
|
80
|
+
# No `pitfalls` / `verification` keys present.
|
|
81
|
+
assert "pitfalls" not in skill
|
|
82
|
+
assert "verification" not in skill
|
|
83
|
+
# Round-trip still works.
|
|
84
|
+
decoded = json.loads(json.dumps(skill))
|
|
85
|
+
assert "pitfalls" not in decoded
|
|
86
|
+
assert "verification" not in decoded
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{awareness_memory_cloud-2.4.4 → awareness_memory_cloud-2.6.0}/memory_cloud/integrations/__init__.py
RENAMED
|
File without changes
|
{awareness_memory_cloud-2.4.4 → awareness_memory_cloud-2.6.0}/memory_cloud/integrations/_base.py
RENAMED
|
File without changes
|
{awareness_memory_cloud-2.4.4 → awareness_memory_cloud-2.6.0}/memory_cloud/integrations/autogen.py
RENAMED
|
File without changes
|
{awareness_memory_cloud-2.4.4 → awareness_memory_cloud-2.6.0}/memory_cloud/integrations/crewai.py
RENAMED
|
File without changes
|
{awareness_memory_cloud-2.4.4 → awareness_memory_cloud-2.6.0}/memory_cloud/integrations/langchain.py
RENAMED
|
File without changes
|
{awareness_memory_cloud-2.4.4 → awareness_memory_cloud-2.6.0}/memory_cloud/integrations/praisonai.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{awareness_memory_cloud-2.4.4 → awareness_memory_cloud-2.6.0}/tests/test_client_chat_and_record.py
RENAMED
|
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
|
{awareness_memory_cloud-2.4.4 → awareness_memory_cloud-2.6.0}/tests/test_interceptor_wrap.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|