maximem-synap-microsoft-agent 0.1.1__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.
- maximem_synap_microsoft_agent-0.1.1/PKG-INFO +72 -0
- maximem_synap_microsoft_agent-0.1.1/README.md +56 -0
- maximem_synap_microsoft_agent-0.1.1/maximem_synap_microsoft_agent.egg-info/PKG-INFO +72 -0
- maximem_synap_microsoft_agent-0.1.1/maximem_synap_microsoft_agent.egg-info/SOURCES.txt +10 -0
- maximem_synap_microsoft_agent-0.1.1/maximem_synap_microsoft_agent.egg-info/dependency_links.txt +1 -0
- maximem_synap_microsoft_agent-0.1.1/maximem_synap_microsoft_agent.egg-info/requires.txt +7 -0
- maximem_synap_microsoft_agent-0.1.1/maximem_synap_microsoft_agent.egg-info/top_level.txt +1 -0
- maximem_synap_microsoft_agent-0.1.1/pyproject.toml +35 -0
- maximem_synap_microsoft_agent-0.1.1/setup.cfg +4 -0
- maximem_synap_microsoft_agent-0.1.1/synap_microsoft_agent/__init__.py +32 -0
- maximem_synap_microsoft_agent-0.1.1/synap_microsoft_agent/context_provider.py +187 -0
- maximem_synap_microsoft_agent-0.1.1/synap_microsoft_agent/history_provider.py +133 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: maximem-synap-microsoft-agent
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Synap memory integration for Microsoft Agent Framework
|
|
5
|
+
Author: Synap Team
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Keywords: synap,memory,microsoft,agent-framework,ai,agents
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Requires-Dist: maximem-synap>=0.2.0
|
|
11
|
+
Requires-Dist: synap-integrations-common>=0.1.0
|
|
12
|
+
Requires-Dist: agent-framework>=1.0
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
15
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
|
|
16
|
+
|
|
17
|
+
# synap-microsoft-agent
|
|
18
|
+
|
|
19
|
+
Synap memory integration for [Microsoft Agent Framework (MAF)](https://learn.microsoft.com/en-us/agent-framework/).
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install synap-microsoft-agent
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Requires `agent-framework>=1.0`, `maximem-synap>=0.2.0`.
|
|
28
|
+
|
|
29
|
+
## Quickstart
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from agent_framework import InMemoryHistoryProvider
|
|
33
|
+
from agent_framework.openai import OpenAIChatClient
|
|
34
|
+
from maximem_synap import MaximemSynapSDK
|
|
35
|
+
from synap_microsoft_agent import SynapContextProvider, SynapHistoryProvider
|
|
36
|
+
|
|
37
|
+
sdk = MaximemSynapSDK(api_key="sk-...")
|
|
38
|
+
client = OpenAIChatClient(model="gpt-4o-mini")
|
|
39
|
+
|
|
40
|
+
agent = client.as_agent(
|
|
41
|
+
name="MemoryAgent",
|
|
42
|
+
instructions="You are a helpful assistant.",
|
|
43
|
+
context_providers=[
|
|
44
|
+
SynapContextProvider(
|
|
45
|
+
sdk=sdk,
|
|
46
|
+
user_id="alice",
|
|
47
|
+
customer_id="acme",
|
|
48
|
+
),
|
|
49
|
+
SynapHistoryProvider(
|
|
50
|
+
sdk=sdk,
|
|
51
|
+
user_id="alice",
|
|
52
|
+
customer_id="acme",
|
|
53
|
+
),
|
|
54
|
+
],
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
session = agent.create_session()
|
|
58
|
+
result = await agent.run("What's my trial expiring?", session=session)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## What each provider does
|
|
62
|
+
|
|
63
|
+
- **`SynapContextProvider`** — on every turn, fetches Synap context (facts, preferences, episodes, emotions, temporal events) and appends it as instructions. After the turn, records the user + assistant messages back to Synap.
|
|
64
|
+
|
|
65
|
+
- **`SynapHistoryProvider`** — persists the conversation message log. Loads prior turns on session resume. Subclass of MAF's `HistoryProvider`, so all its flags (`load_messages`, `store_inputs`, `store_outputs`, `store_context_messages`) work as documented.
|
|
66
|
+
|
|
67
|
+
You can use either or both; they coexist.
|
|
68
|
+
|
|
69
|
+
## Error policy
|
|
70
|
+
|
|
71
|
+
- **Read-side failures** (`fetch`, `get_context_for_prompt`) degrade gracefully — logged at `ERROR`, empty result returned. An outage never crashes an agent turn.
|
|
72
|
+
- **Write-side failures** — `SynapContextProvider.after_run` logs and swallows (context providers must not raise per MAF's hook contract). `SynapHistoryProvider.save_messages` surfaces errors as `SynapIntegrationError` so explicit persistence failures are observable.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# synap-microsoft-agent
|
|
2
|
+
|
|
3
|
+
Synap memory integration for [Microsoft Agent Framework (MAF)](https://learn.microsoft.com/en-us/agent-framework/).
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install synap-microsoft-agent
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Requires `agent-framework>=1.0`, `maximem-synap>=0.2.0`.
|
|
12
|
+
|
|
13
|
+
## Quickstart
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from agent_framework import InMemoryHistoryProvider
|
|
17
|
+
from agent_framework.openai import OpenAIChatClient
|
|
18
|
+
from maximem_synap import MaximemSynapSDK
|
|
19
|
+
from synap_microsoft_agent import SynapContextProvider, SynapHistoryProvider
|
|
20
|
+
|
|
21
|
+
sdk = MaximemSynapSDK(api_key="sk-...")
|
|
22
|
+
client = OpenAIChatClient(model="gpt-4o-mini")
|
|
23
|
+
|
|
24
|
+
agent = client.as_agent(
|
|
25
|
+
name="MemoryAgent",
|
|
26
|
+
instructions="You are a helpful assistant.",
|
|
27
|
+
context_providers=[
|
|
28
|
+
SynapContextProvider(
|
|
29
|
+
sdk=sdk,
|
|
30
|
+
user_id="alice",
|
|
31
|
+
customer_id="acme",
|
|
32
|
+
),
|
|
33
|
+
SynapHistoryProvider(
|
|
34
|
+
sdk=sdk,
|
|
35
|
+
user_id="alice",
|
|
36
|
+
customer_id="acme",
|
|
37
|
+
),
|
|
38
|
+
],
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
session = agent.create_session()
|
|
42
|
+
result = await agent.run("What's my trial expiring?", session=session)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## What each provider does
|
|
46
|
+
|
|
47
|
+
- **`SynapContextProvider`** — on every turn, fetches Synap context (facts, preferences, episodes, emotions, temporal events) and appends it as instructions. After the turn, records the user + assistant messages back to Synap.
|
|
48
|
+
|
|
49
|
+
- **`SynapHistoryProvider`** — persists the conversation message log. Loads prior turns on session resume. Subclass of MAF's `HistoryProvider`, so all its flags (`load_messages`, `store_inputs`, `store_outputs`, `store_context_messages`) work as documented.
|
|
50
|
+
|
|
51
|
+
You can use either or both; they coexist.
|
|
52
|
+
|
|
53
|
+
## Error policy
|
|
54
|
+
|
|
55
|
+
- **Read-side failures** (`fetch`, `get_context_for_prompt`) degrade gracefully — logged at `ERROR`, empty result returned. An outage never crashes an agent turn.
|
|
56
|
+
- **Write-side failures** — `SynapContextProvider.after_run` logs and swallows (context providers must not raise per MAF's hook contract). `SynapHistoryProvider.save_messages` surfaces errors as `SynapIntegrationError` so explicit persistence failures are observable.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: maximem-synap-microsoft-agent
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Synap memory integration for Microsoft Agent Framework
|
|
5
|
+
Author: Synap Team
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Keywords: synap,memory,microsoft,agent-framework,ai,agents
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Requires-Dist: maximem-synap>=0.2.0
|
|
11
|
+
Requires-Dist: synap-integrations-common>=0.1.0
|
|
12
|
+
Requires-Dist: agent-framework>=1.0
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
15
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
|
|
16
|
+
|
|
17
|
+
# synap-microsoft-agent
|
|
18
|
+
|
|
19
|
+
Synap memory integration for [Microsoft Agent Framework (MAF)](https://learn.microsoft.com/en-us/agent-framework/).
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install synap-microsoft-agent
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Requires `agent-framework>=1.0`, `maximem-synap>=0.2.0`.
|
|
28
|
+
|
|
29
|
+
## Quickstart
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from agent_framework import InMemoryHistoryProvider
|
|
33
|
+
from agent_framework.openai import OpenAIChatClient
|
|
34
|
+
from maximem_synap import MaximemSynapSDK
|
|
35
|
+
from synap_microsoft_agent import SynapContextProvider, SynapHistoryProvider
|
|
36
|
+
|
|
37
|
+
sdk = MaximemSynapSDK(api_key="sk-...")
|
|
38
|
+
client = OpenAIChatClient(model="gpt-4o-mini")
|
|
39
|
+
|
|
40
|
+
agent = client.as_agent(
|
|
41
|
+
name="MemoryAgent",
|
|
42
|
+
instructions="You are a helpful assistant.",
|
|
43
|
+
context_providers=[
|
|
44
|
+
SynapContextProvider(
|
|
45
|
+
sdk=sdk,
|
|
46
|
+
user_id="alice",
|
|
47
|
+
customer_id="acme",
|
|
48
|
+
),
|
|
49
|
+
SynapHistoryProvider(
|
|
50
|
+
sdk=sdk,
|
|
51
|
+
user_id="alice",
|
|
52
|
+
customer_id="acme",
|
|
53
|
+
),
|
|
54
|
+
],
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
session = agent.create_session()
|
|
58
|
+
result = await agent.run("What's my trial expiring?", session=session)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## What each provider does
|
|
62
|
+
|
|
63
|
+
- **`SynapContextProvider`** — on every turn, fetches Synap context (facts, preferences, episodes, emotions, temporal events) and appends it as instructions. After the turn, records the user + assistant messages back to Synap.
|
|
64
|
+
|
|
65
|
+
- **`SynapHistoryProvider`** — persists the conversation message log. Loads prior turns on session resume. Subclass of MAF's `HistoryProvider`, so all its flags (`load_messages`, `store_inputs`, `store_outputs`, `store_context_messages`) work as documented.
|
|
66
|
+
|
|
67
|
+
You can use either or both; they coexist.
|
|
68
|
+
|
|
69
|
+
## Error policy
|
|
70
|
+
|
|
71
|
+
- **Read-side failures** (`fetch`, `get_context_for_prompt`) degrade gracefully — logged at `ERROR`, empty result returned. An outage never crashes an agent turn.
|
|
72
|
+
- **Write-side failures** — `SynapContextProvider.after_run` logs and swallows (context providers must not raise per MAF's hook contract). `SynapHistoryProvider.save_messages` surfaces errors as `SynapIntegrationError` so explicit persistence failures are observable.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
maximem_synap_microsoft_agent.egg-info/PKG-INFO
|
|
4
|
+
maximem_synap_microsoft_agent.egg-info/SOURCES.txt
|
|
5
|
+
maximem_synap_microsoft_agent.egg-info/dependency_links.txt
|
|
6
|
+
maximem_synap_microsoft_agent.egg-info/requires.txt
|
|
7
|
+
maximem_synap_microsoft_agent.egg-info/top_level.txt
|
|
8
|
+
synap_microsoft_agent/__init__.py
|
|
9
|
+
synap_microsoft_agent/context_provider.py
|
|
10
|
+
synap_microsoft_agent/history_provider.py
|
maximem_synap_microsoft_agent-0.1.1/maximem_synap_microsoft_agent.egg-info/dependency_links.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
synap_microsoft_agent
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "maximem-synap-microsoft-agent"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
description = "Synap memory integration for Microsoft Agent Framework"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "Apache-2.0"
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "Synap Team"}
|
|
14
|
+
]
|
|
15
|
+
keywords = ["synap", "memory", "microsoft", "agent-framework", "ai", "agents"]
|
|
16
|
+
|
|
17
|
+
dependencies = [
|
|
18
|
+
"maximem-synap>=0.2.0",
|
|
19
|
+
"synap-integrations-common>=0.1.0",
|
|
20
|
+
"agent-framework>=1.0",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.optional-dependencies]
|
|
24
|
+
dev = [
|
|
25
|
+
"pytest>=7.0",
|
|
26
|
+
"pytest-asyncio>=0.21",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[tool.setuptools.packages.find]
|
|
30
|
+
where = ["."]
|
|
31
|
+
include = ["synap_microsoft_agent*"]
|
|
32
|
+
|
|
33
|
+
[tool.pytest.ini_options]
|
|
34
|
+
testpaths = ["tests"]
|
|
35
|
+
asyncio_mode = "auto"
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Synap integration for Microsoft Agent Framework (MAF).
|
|
2
|
+
|
|
3
|
+
Two plug points into MAF's context-engineering pipeline:
|
|
4
|
+
|
|
5
|
+
- :class:`SynapContextProvider` — subclass of ``ContextProvider``. Injects
|
|
6
|
+
Synap-sourced context into the agent's instructions on every turn via
|
|
7
|
+
``before_run``, and records each turn back to Synap via ``after_run``.
|
|
8
|
+
Pattern mirrors ``agent_framework.mem0.Mem0ContextProvider``.
|
|
9
|
+
|
|
10
|
+
- :class:`SynapHistoryProvider` — subclass of ``HistoryProvider``. Persists
|
|
11
|
+
the conversation message log to Synap via ``sdk.conversation.record_message``
|
|
12
|
+
and loads it back via ``sdk.conversation.context.get_context_for_prompt``.
|
|
13
|
+
|
|
14
|
+
Typical wiring::
|
|
15
|
+
|
|
16
|
+
from agent_framework import InMemoryHistoryProvider
|
|
17
|
+
from synap_microsoft_agent import SynapContextProvider
|
|
18
|
+
|
|
19
|
+
agent = client.as_agent(
|
|
20
|
+
name="MemoryAgent",
|
|
21
|
+
instructions="You are a helpful assistant.",
|
|
22
|
+
context_providers=[
|
|
23
|
+
SynapContextProvider(sdk=sdk, user_id="alice", customer_id="acme"),
|
|
24
|
+
InMemoryHistoryProvider(load_messages=True),
|
|
25
|
+
],
|
|
26
|
+
)
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from synap_microsoft_agent.context_provider import SynapContextProvider
|
|
30
|
+
from synap_microsoft_agent.history_provider import SynapHistoryProvider
|
|
31
|
+
|
|
32
|
+
__all__ = ["SynapContextProvider", "SynapHistoryProvider"]
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""SynapContextProvider — MAF ContextProvider backed by Synap.
|
|
2
|
+
|
|
3
|
+
Pattern mirrors ``agent_framework.mem0.Mem0ContextProvider``:
|
|
4
|
+
|
|
5
|
+
- ``before_run`` builds a query from ``context.input_messages`` text, calls
|
|
6
|
+
``sdk.fetch(...)``, and appends the formatted result as instructions via
|
|
7
|
+
``context.extend_instructions(source_id, ...)``. Read failures degrade
|
|
8
|
+
gracefully — we log + skip rather than breaking the agent turn.
|
|
9
|
+
|
|
10
|
+
- ``after_run`` records each input + response message to Synap via
|
|
11
|
+
``sdk.conversation.record_message(...)``. Write failures are logged but
|
|
12
|
+
never re-raised, following MAF's "context providers should not crash the
|
|
13
|
+
agent" contract (same as LangChain's ``SynapCallbackHandler``).
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import logging
|
|
19
|
+
from typing import Any, ClassVar, Optional
|
|
20
|
+
|
|
21
|
+
from agent_framework import ContextProvider
|
|
22
|
+
from maximem_synap import MaximemSynapSDK
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class SynapContextProvider(ContextProvider):
|
|
28
|
+
"""Inject Synap context + record turns back to Synap."""
|
|
29
|
+
|
|
30
|
+
DEFAULT_SOURCE_ID: ClassVar[str] = "synap"
|
|
31
|
+
DEFAULT_CONTEXT_PROMPT: ClassVar[str] = (
|
|
32
|
+
"## User Memory Context\n"
|
|
33
|
+
"Consider the following context about the user and their history "
|
|
34
|
+
"when answering. Treat it as background knowledge, not direct input."
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
sdk: MaximemSynapSDK,
|
|
40
|
+
user_id: str,
|
|
41
|
+
customer_id: str = "",
|
|
42
|
+
conversation_id: Optional[str] = None,
|
|
43
|
+
*,
|
|
44
|
+
source_id: str = DEFAULT_SOURCE_ID,
|
|
45
|
+
mode: str = "accurate",
|
|
46
|
+
max_results: int = 20,
|
|
47
|
+
context_prompt: Optional[str] = None,
|
|
48
|
+
include_scope_labels: bool = False,
|
|
49
|
+
) -> None:
|
|
50
|
+
if sdk is None:
|
|
51
|
+
raise ValueError("SynapContextProvider requires a non-None sdk")
|
|
52
|
+
if not user_id:
|
|
53
|
+
raise ValueError("SynapContextProvider requires a non-empty user_id")
|
|
54
|
+
|
|
55
|
+
super().__init__(source_id)
|
|
56
|
+
self.sdk = sdk
|
|
57
|
+
self.user_id = user_id
|
|
58
|
+
self.customer_id = customer_id
|
|
59
|
+
self.conversation_id = conversation_id
|
|
60
|
+
self.mode = mode
|
|
61
|
+
self.max_results = max_results
|
|
62
|
+
self.context_prompt = context_prompt or self.DEFAULT_CONTEXT_PROMPT
|
|
63
|
+
self.include_scope_labels = include_scope_labels
|
|
64
|
+
|
|
65
|
+
async def before_run(
|
|
66
|
+
self,
|
|
67
|
+
*,
|
|
68
|
+
agent: Any,
|
|
69
|
+
session: Any,
|
|
70
|
+
context: Any,
|
|
71
|
+
state: dict[str, Any],
|
|
72
|
+
) -> None:
|
|
73
|
+
"""Fetch Synap context and append it to the agent's instructions."""
|
|
74
|
+
input_text = self._concat_text(context.input_messages)
|
|
75
|
+
if not input_text:
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
conv_id = self._resolve_conversation_id(context)
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
response = await self.sdk.fetch(
|
|
82
|
+
conversation_id=conv_id,
|
|
83
|
+
user_id=self.user_id,
|
|
84
|
+
customer_id=self.customer_id or None,
|
|
85
|
+
search_query=[input_text],
|
|
86
|
+
max_results=self.max_results,
|
|
87
|
+
mode=self.mode,
|
|
88
|
+
include_conversation_context=False,
|
|
89
|
+
include_scope_labels=self.include_scope_labels,
|
|
90
|
+
)
|
|
91
|
+
except Exception as exc: # noqa: BLE001 — read-side degrades gracefully
|
|
92
|
+
logger.error(
|
|
93
|
+
"SynapContextProvider.before_run: sdk.fetch failed "
|
|
94
|
+
"user_id=%s conversation_id=%s error=%s",
|
|
95
|
+
self.user_id,
|
|
96
|
+
conv_id,
|
|
97
|
+
exc,
|
|
98
|
+
exc_info=True,
|
|
99
|
+
)
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
formatted = (response.formatted_context or "").strip()
|
|
103
|
+
if not formatted:
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
context.extend_instructions(
|
|
107
|
+
self.source_id,
|
|
108
|
+
f"{self.context_prompt}\n{formatted}",
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
async def after_run(
|
|
112
|
+
self,
|
|
113
|
+
*,
|
|
114
|
+
agent: Any,
|
|
115
|
+
session: Any,
|
|
116
|
+
context: Any,
|
|
117
|
+
state: dict[str, Any],
|
|
118
|
+
) -> None:
|
|
119
|
+
"""Record input + response messages to Synap conversation history."""
|
|
120
|
+
conv_id = self._resolve_conversation_id(context)
|
|
121
|
+
if not conv_id:
|
|
122
|
+
# Without a conversation id we can't attribute the turn — skip.
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
to_record: list[tuple[str, str]] = []
|
|
126
|
+
for message in context.input_messages or []:
|
|
127
|
+
role, text = self._message_to_role_text(message)
|
|
128
|
+
if role and text:
|
|
129
|
+
to_record.append((role, text))
|
|
130
|
+
|
|
131
|
+
response = context.response
|
|
132
|
+
if response is not None and getattr(response, "messages", None):
|
|
133
|
+
for message in response.messages:
|
|
134
|
+
role, text = self._message_to_role_text(message)
|
|
135
|
+
if role and text:
|
|
136
|
+
to_record.append((role, text))
|
|
137
|
+
|
|
138
|
+
for role, text in to_record:
|
|
139
|
+
try:
|
|
140
|
+
await self.sdk.conversation.record_message(
|
|
141
|
+
conversation_id=conv_id,
|
|
142
|
+
role=role,
|
|
143
|
+
content=text,
|
|
144
|
+
user_id=self.user_id,
|
|
145
|
+
customer_id=self.customer_id,
|
|
146
|
+
)
|
|
147
|
+
except Exception as exc: # noqa: BLE001 — must not crash the agent
|
|
148
|
+
logger.error(
|
|
149
|
+
"SynapContextProvider.after_run: record_message failed "
|
|
150
|
+
"role=%s conversation_id=%s error=%s",
|
|
151
|
+
role,
|
|
152
|
+
conv_id,
|
|
153
|
+
exc,
|
|
154
|
+
exc_info=True,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# -- helpers --------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
def _resolve_conversation_id(self, context: Any) -> Optional[str]:
|
|
160
|
+
if self.conversation_id:
|
|
161
|
+
return self.conversation_id
|
|
162
|
+
sid = getattr(context, "session_id", None)
|
|
163
|
+
return sid if sid else None
|
|
164
|
+
|
|
165
|
+
@staticmethod
|
|
166
|
+
def _concat_text(messages: Any) -> str:
|
|
167
|
+
if not messages:
|
|
168
|
+
return ""
|
|
169
|
+
parts: list[str] = []
|
|
170
|
+
for msg in messages:
|
|
171
|
+
text = getattr(msg, "text", None)
|
|
172
|
+
if text and text.strip():
|
|
173
|
+
parts.append(text.strip())
|
|
174
|
+
return "\n".join(parts)
|
|
175
|
+
|
|
176
|
+
@staticmethod
|
|
177
|
+
def _message_to_role_text(message: Any) -> tuple[Optional[str], str]:
|
|
178
|
+
role_raw = getattr(message, "role", None)
|
|
179
|
+
if role_raw is None:
|
|
180
|
+
return None, ""
|
|
181
|
+
role = role_raw.value if hasattr(role_raw, "value") else str(role_raw)
|
|
182
|
+
if role not in {"user", "assistant", "system"}:
|
|
183
|
+
return None, ""
|
|
184
|
+
text = getattr(message, "text", "") or ""
|
|
185
|
+
if not text.strip():
|
|
186
|
+
return None, ""
|
|
187
|
+
return role, text
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""SynapHistoryProvider — MAF HistoryProvider backed by Synap.
|
|
2
|
+
|
|
3
|
+
Persists conversation messages to Synap and loads them back into the MAF
|
|
4
|
+
session. Wrap one or more of these in ``context_providers`` to get durable,
|
|
5
|
+
cross-process conversation memory.
|
|
6
|
+
|
|
7
|
+
Load path (``get_messages``) reads via ``sdk.conversation.context.get_context_for_prompt``
|
|
8
|
+
and degrades gracefully on failure (returns ``[]``) — a history read outage
|
|
9
|
+
shouldn't abort the agent turn.
|
|
10
|
+
|
|
11
|
+
Save path (``save_messages``) iterates ``sdk.conversation.record_message``
|
|
12
|
+
and surfaces SDK errors as :class:`SynapIntegrationError` because silent drops
|
|
13
|
+
would hide ingestion problems from the caller.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import logging
|
|
19
|
+
from typing import Any, ClassVar, Optional, Sequence
|
|
20
|
+
|
|
21
|
+
from agent_framework import Message
|
|
22
|
+
from agent_framework._sessions import HistoryProvider
|
|
23
|
+
from maximem_synap import MaximemSynapSDK
|
|
24
|
+
from synap_integrations_common import wrap_sdk_errors_async
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class SynapHistoryProvider(HistoryProvider):
|
|
30
|
+
"""Store + load MAF conversation history via Synap."""
|
|
31
|
+
|
|
32
|
+
DEFAULT_SOURCE_ID: ClassVar[str] = "synap_history"
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
sdk: MaximemSynapSDK,
|
|
37
|
+
user_id: str,
|
|
38
|
+
customer_id: str = "",
|
|
39
|
+
conversation_id: Optional[str] = None,
|
|
40
|
+
*,
|
|
41
|
+
source_id: str = DEFAULT_SOURCE_ID,
|
|
42
|
+
load_messages: bool = True,
|
|
43
|
+
store_inputs: bool = True,
|
|
44
|
+
store_context_messages: bool = False,
|
|
45
|
+
store_context_from: Optional[set[str]] = None,
|
|
46
|
+
store_outputs: bool = True,
|
|
47
|
+
) -> None:
|
|
48
|
+
if sdk is None:
|
|
49
|
+
raise ValueError("SynapHistoryProvider requires a non-None sdk")
|
|
50
|
+
if not user_id:
|
|
51
|
+
raise ValueError("SynapHistoryProvider requires a non-empty user_id")
|
|
52
|
+
|
|
53
|
+
super().__init__(
|
|
54
|
+
source_id,
|
|
55
|
+
load_messages=load_messages,
|
|
56
|
+
store_inputs=store_inputs,
|
|
57
|
+
store_context_messages=store_context_messages,
|
|
58
|
+
store_context_from=store_context_from,
|
|
59
|
+
store_outputs=store_outputs,
|
|
60
|
+
)
|
|
61
|
+
self.sdk = sdk
|
|
62
|
+
self.user_id = user_id
|
|
63
|
+
self.customer_id = customer_id
|
|
64
|
+
self.conversation_id = conversation_id
|
|
65
|
+
|
|
66
|
+
async def get_messages(
|
|
67
|
+
self,
|
|
68
|
+
session_id: Optional[str],
|
|
69
|
+
*,
|
|
70
|
+
state: Optional[dict[str, Any]] = None,
|
|
71
|
+
**kwargs: Any,
|
|
72
|
+
) -> list[Message]:
|
|
73
|
+
conv_id = self.conversation_id or session_id
|
|
74
|
+
if not conv_id:
|
|
75
|
+
return []
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
response = await self.sdk.conversation.context.get_context_for_prompt(
|
|
79
|
+
conversation_id=conv_id,
|
|
80
|
+
)
|
|
81
|
+
except Exception as exc: # noqa: BLE001 — read-side degrades gracefully
|
|
82
|
+
logger.error(
|
|
83
|
+
"SynapHistoryProvider.get_messages: get_context_for_prompt failed "
|
|
84
|
+
"conversation_id=%s error=%s",
|
|
85
|
+
conv_id,
|
|
86
|
+
exc,
|
|
87
|
+
exc_info=True,
|
|
88
|
+
)
|
|
89
|
+
return []
|
|
90
|
+
|
|
91
|
+
recent = getattr(response, "recent_messages", None) or []
|
|
92
|
+
messages: list[Message] = []
|
|
93
|
+
for rm in recent:
|
|
94
|
+
role = getattr(rm, "role", None)
|
|
95
|
+
content = getattr(rm, "content", None)
|
|
96
|
+
if not role or not content:
|
|
97
|
+
continue
|
|
98
|
+
messages.append(Message(role=role, contents=[content]))
|
|
99
|
+
return messages
|
|
100
|
+
|
|
101
|
+
async def save_messages(
|
|
102
|
+
self,
|
|
103
|
+
session_id: Optional[str],
|
|
104
|
+
messages: Sequence[Message],
|
|
105
|
+
*,
|
|
106
|
+
state: Optional[dict[str, Any]] = None,
|
|
107
|
+
**kwargs: Any,
|
|
108
|
+
) -> None:
|
|
109
|
+
conv_id = self.conversation_id or session_id
|
|
110
|
+
if not conv_id or not messages:
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
async with wrap_sdk_errors_async(
|
|
114
|
+
"microsoft_agent.save_messages",
|
|
115
|
+
logger,
|
|
116
|
+
user_id=self.user_id,
|
|
117
|
+
conversation_id=conv_id,
|
|
118
|
+
):
|
|
119
|
+
for message in messages:
|
|
120
|
+
role_raw = getattr(message, "role", None)
|
|
121
|
+
role = role_raw.value if hasattr(role_raw, "value") else str(role_raw)
|
|
122
|
+
if role not in {"user", "assistant", "system"}:
|
|
123
|
+
continue
|
|
124
|
+
text = getattr(message, "text", "") or ""
|
|
125
|
+
if not text.strip():
|
|
126
|
+
continue
|
|
127
|
+
await self.sdk.conversation.record_message(
|
|
128
|
+
conversation_id=conv_id,
|
|
129
|
+
role=role,
|
|
130
|
+
content=text,
|
|
131
|
+
user_id=self.user_id,
|
|
132
|
+
customer_id=self.customer_id,
|
|
133
|
+
)
|