Skip to content

fix(langgraph): normalize episode type strings#1344

Closed
haosenwang1018 wants to merge 4 commits into
MemMachine:mainfrom
haosenwang1018:fix/langgraph-episode-type-normalization
Closed

fix(langgraph): normalize episode type strings#1344
haosenwang1018 wants to merge 4 commits into
MemMachine:mainfrom
haosenwang1018:fix/langgraph-episode-type-normalization

Conversation

@haosenwang1018

Copy link
Copy Markdown
Contributor

Purpose of the change

Fix the LangGraph integration so string episode_type values are normalized before being passed into the Python client memory API.

Description

Fixes #1002

The current LangGraph demo/integration path can pass episode_type as a string, but the Python client expects an EpisodeType enum in Memory.add(). That mismatch causes runtime failures like:

  • AttributeError: 'str' object has no attribute 'value'

This change updates the LangGraph wrapper to accept episode_type: EpisodeType | str | None and normalize strings via EpisodeType(...) before calling memory.add(...).

Also updated tests in packages/client/client_tests/test_langgraph.py to cover:

  • default passthrough with episode_type=None
  • string episode_type normalization to EpisodeType.MESSAGE
  • compatibility of the convenience add-memory tool signature

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Refactor (does not change functionality, e.g., code style improvements, linting)
  • Documentation update
  • Project Maintenance (updates to build scripts, CI, etc., that do not affect the main project)
  • Security (improves security without changing functionality)

How Has This Been Tested?

  • Unit Test
  • Integration Test
  • End-to-end Test
  • Test Script (please provide)
  • Manual verification (list step-by-step instructions)

Test command:

  • cd packages/client && PYTHONPATH=/home/node/.openclaw/workspace/MemMachine/packages/client/src:/home/node/.openclaw/workspace/MemMachine/packages/common/src python -m pytest client_tests/test_langgraph.py -q

Checklist

  • I have signed the commit(s) within this pull request
  • My code follows the style guidelines of this project (See STYLE_GUIDE.md)
  • I have performed a self-review of my own code
  • I have commented my code
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added unit tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published in downstream modules
  • I have checked my code and corrected any misspellings

Maintainer Checklist

  • Confirmed all checks passed
  • Contributor has signed the commit(s)
  • Reviewed the code
  • Run, Tested, and Verified the change(s) work as expected

@sscargal sscargal requested a review from Copilot April 17, 2026 17:29
@sscargal

Copy link
Copy Markdown
Contributor

@haosenwang1018 thank you for the pull request submission. Please sign your commits, resolve the unit test failures, and review the CoPilot feedback. You only need to resolve the relevant items. Thanks.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Fixes LangGraph → client type mismatches and expands the client’s filtering API to accept raw filter strings in addition to key/value filter_dict.

Changes:

  • Normalize episode_type when passed as a string in LangGraph tools before calling Memory.add().
  • Add optional raw filter string support to Memory.search() and Memory.list() and thread it through LangGraph search tooling.
  • Update unit/integration tests to validate raw-filter composition and updated filter_dict key conventions (metadata.*).

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
packages/client/src/memmachine_client/memory.py Adds raw filter parameter and combines it with built-in + filter_dict filters.
packages/client/src/memmachine_client/langgraph.py Normalizes episode_type and forwards raw filter into Memory.search().
packages/client/client_tests/test_memory.py Adds coverage for raw filter strings; adjusts filter_dict keys to metadata.*.
packages/client/client_tests/test_langgraph.py Adds coverage for episode_type string normalization and raw filter passthrough.
packages/client/client_tests/test_integration_complete.py Updates integration test to use metadata.* keys in filter_dict.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 462 to 467
page_size: int = 100,
page_num: int = 0,
filter_dict: dict[str, str] | None = None,
filter: str | None = None,
set_metadata: dict[str, JsonValue] | None = None,
timeout: int | None = None,

Copilot AI Apr 17, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding filter in the middle of list(...)’s positional parameters is a backward-compatibility hazard: callers passing set_metadata/timeout positionally will now bind to filter/set_metadata instead. Make filter keyword-only (e.g., add a * before filter) or move filter to the end of the signature to avoid breaking positional call sites.

Copilot uses AI. Check for mistakes.
Comment on lines +418 to +424
dict_filter_str = self._dict_to_filter_string(merged_filters) if merged_filters else ""
explicit_filter = filter.strip() if filter else ""

if dict_filter_str and explicit_filter:
filter_str = f"{dict_filter_str} AND ({explicit_filter})"
else:
filter_str = dict_filter_str or explicit_filter

Copilot AI Apr 17, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When both built-in filters and user filters are absent, filter_str becomes an empty string, which will still be serialized (it’s not None) and sent to the API as filter: \"\". If the server treats empty filter as invalid (or different from missing), this can cause unexpected failures/behavior. Prefer passing None when the final filter is empty (e.g., filter = filter_str or None) and apply the same fix in both search() and list().

Copilot uses AI. Check for mistakes.
filter_dict: dict[str, str] | None = None,
timeout: int | None = None,
*,
filter: str | None = None,

Copilot AI Apr 17, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR title/description focuses on normalizing episode_type, but this PR also introduces a new raw filter parameter and filter-composition behavior in Memory.search()/Memory.list() and LangGraph tooling. Please either (a) update the PR title/description to include the raw-filter feature, or (b) split the raw-filter addition into a separate PR to keep scope aligned.

Copilot uses AI. Check for mistakes.
Comment on lines +175 to +179
normalized_episode_type = (
EpisodeType(episode_type)
if isinstance(episode_type, str)
else episode_type
)

Copilot AI Apr 17, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EpisodeType(episode_type) will raise a bare ValueError for unsupported strings, which can be hard to diagnose for LangGraph users. Consider catching that exception and re-raising with a clearer message that includes the provided value and the allowed EpisodeType values (or normalize common variants like enum names/casing if you intend to accept them).

Copilot uses AI. Check for mistakes.
Comment on lines +496 to +502
dict_filter_str = self._dict_to_filter_string(merged_filters) if merged_filters else ""
explicit_filter = filter.strip() if filter else ""

if dict_filter_str and explicit_filter:
filter_str = f"{dict_filter_str} AND ({explicit_filter})"
else:
filter_str = dict_filter_str or explicit_filter

Copilot AI Apr 17, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The filter-combination logic is duplicated in both search() and list(). To reduce drift (e.g., future changes to parenthesization/empty handling), consider extracting a small helper (private method/function) that takes merged_filters + filter and returns the final filter string/None.

Copilot uses AI. Check for mistakes.
@sscargal sscargal added this to the v0.3.9 milestone May 13, 2026
sscargal added a commit to sscargal/MemMachine that referenced this pull request May 13, 2026
Add three small features to the Python client and its LangGraph wrapper
so callers can pass structured filter expressions and either-enum-or-string
episode types directly.

1. `Memory.search(filter=...)` and `Memory.list(filter=...)`
   Accept an optional raw filter string alongside `filter_dict`. When both
   are provided, the two are combined with `AND`. The raw filter is passed
   through to the v2 `SearchMemoriesSpec.filter` / `ListMemoriesSpec.filter`
   fields unchanged.

2. `MemMachineTools.search_memory(filter=...)`
   Pipes the same raw filter through the LangGraph search-memory tool.

3. `MemMachineTools.add_memory(episode_type=...)`
   Accept either an `EpisodeType` enum or its string value
   (e.g. `"message"`), normalizing strings via `EpisodeType(...)` before
   delegating to `Memory.add`. The factory tool's return-type annotation
   was widened to match.

The `filter` parameter shadows the Python builtin, which is the same
trade-off `memmachine_common.api.SearchMemoriesSpec` already made for
its `filter:` field — keeping the parameter name aligned with the API
field. `# noqa: A002` is applied at the three call sites with a comment
pointing at the API spec.

This commit consolidates the substantive work from haosenwang1018's
9-commit stack (MemMachine#1341MemMachine#1349) into a single rebased+linted commit
against current `main`. The original stack's prefix-style doc and test
changes have been omitted because they have already landed on `main`
via MemMachine#1352 and MemMachine#1311. The original commits authored by haosenwang1018:

  - 921b55f feat(client): support raw filter strings
  - e0849bb feat(langgraph): support raw filter strings
  - 53b489e fix(langgraph): normalize episode type strings

Closes MemMachine#1341, MemMachine#1342, MemMachine#1343, MemMachine#1344, MemMachine#1345, MemMachine#1346, MemMachine#1347, MemMachine#1348, MemMachine#1349

Co-authored-by: Steve Scargall <steve.scargall@gmail.com>
Signed-off-by: Steve Scargall <37674041+sscargal@users.noreply.github.com>
malatewang added a commit that referenced this pull request May 13, 2026
…ion (#1403)

* feat(client+langgraph): raw filter strings and EpisodeType normalization

Add three small features to the Python client and its LangGraph wrapper
so callers can pass structured filter expressions and either-enum-or-string
episode types directly.

1. `Memory.search(filter=...)` and `Memory.list(filter=...)`
   Accept an optional raw filter string alongside `filter_dict`. When both
   are provided, the two are combined with `AND`. The raw filter is passed
   through to the v2 `SearchMemoriesSpec.filter` / `ListMemoriesSpec.filter`
   fields unchanged.

2. `MemMachineTools.search_memory(filter=...)`
   Pipes the same raw filter through the LangGraph search-memory tool.

3. `MemMachineTools.add_memory(episode_type=...)`
   Accept either an `EpisodeType` enum or its string value
   (e.g. `"message"`), normalizing strings via `EpisodeType(...)` before
   delegating to `Memory.add`. The factory tool's return-type annotation
   was widened to match.

The `filter` parameter shadows the Python builtin, which is the same
trade-off `memmachine_common.api.SearchMemoriesSpec` already made for
its `filter:` field — keeping the parameter name aligned with the API
field. `# noqa: A002` is applied at the three call sites with a comment
pointing at the API spec.

This commit consolidates the substantive work from haosenwang1018's
9-commit stack (#1341#1349) into a single rebased+linted commit
against current `main`. The original stack's prefix-style doc and test
changes have been omitted because they have already landed on `main`
via #1352 and #1311. The original commits authored by haosenwang1018:

  - 921b55f feat(client): support raw filter strings
  - e0849bb feat(langgraph): support raw filter strings
  - 53b489e fix(langgraph): normalize episode type strings

Closes #1341, #1342, #1343, #1344, #1345, #1346, #1347, #1348, #1349

Co-authored-by: Steve Scargall <steve.scargall@gmail.com>
Signed-off-by: Steve Scargall <37674041+sscargal@users.noreply.github.com>

* docs(langgraph): document filter and episode type support

---------

Signed-off-by: Steve Scargall <37674041+sscargal@users.noreply.github.com>
Co-authored-by: haosenwang1018 <haosenwang1018@users.noreply.github.com>
Co-authored-by: Shu Wang <33640803+malatewang@users.noreply.github.com>
@sscargal

Copy link
Copy Markdown
Contributor

Closed by #1403

@sscargal sscargal closed this May 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: LangGraph example demo raises AttributeError when adding memories with v2 APIs

3 participants