@macaw-ai/client 0.0.0-stage → 0.9.10
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.
- package/LICENSE.md +339 -0
- package/bin/install.js +278 -0
- package/package.json +37 -4
- package/payload/README.md +56 -0
- package/payload/secureAI/LICENSE +190 -0
- package/payload/secureAI/NOTICE +5 -0
- package/payload/secureAI/demos/policy-controls/1_uncontrolled.py +139 -0
- package/payload/secureAI/demos/policy-controls/2_with_macaw.py +139 -0
- package/payload/secureAI/demos/policy-controls/3_with_user_policies.py +162 -0
- package/payload/secureAI/demos/policy-controls/credentials.txt +12 -0
- package/payload/secureAI/demos/policy-controls/malicious_report.txt +21 -0
- package/payload/secureAI/demos/policy-controls/sales_report.txt +20 -0
- package/payload/secureAI/demos/tutorial-1-rbac/README.md +143 -0
- package/payload/secureAI/demos/tutorial-1-rbac/app.py +147 -0
- package/payload/secureAI/demos/tutorial-1-rbac/authprompts_demo.py +306 -0
- package/payload/secureAI/demos/tutorial-1-rbac/config/claims-config.yaml +152 -0
- package/payload/secureAI/demos/tutorial-1-rbac/demo_secureopenai_with_policies.py +150 -0
- package/payload/secureAI/demos/tutorial-1-rbac/policies/bu_Analytics.json +8 -0
- package/payload/secureAI/demos/tutorial-1-rbac/policies/company_FinTech_Corp.json +70 -0
- package/payload/secureAI/demos/tutorial-1-rbac/policies/llm-base.json +69 -0
- package/payload/secureAI/demos/tutorial-1-rbac/policies/team_Reporting.json +8 -0
- package/payload/secureAI/demos/tutorial-1-rbac/policies/user_alice.json +32 -0
- package/payload/secureAI/demos/tutorial-1-rbac/policies/user_bob.json +28 -0
- package/payload/secureAI/demos/tutorial-1-rbac/policies/user_carol.json +34 -0
- package/payload/secureAI/demos/tutorial-1-rbac/requirements.txt +6 -0
- package/payload/secureAI/demos/tutorial-1-rbac/setup/auth0_complete_setup.sh +392 -0
- package/payload/secureAI/demos/tutorial-1-rbac/setup/keycloak_complete_setup.sh +320 -0
- package/payload/secureAI/demos/tutorial-2-intent/Policies/alation-remote-proxy.json +155 -0
- package/payload/secureAI/demos/tutorial-2-intent/Policies/bu.json +22 -0
- package/payload/secureAI/demos/tutorial-2-intent/Policies/company.json +17 -0
- package/payload/secureAI/demos/tutorial-2-intent/Policies/user_alice.json +48 -0
- package/payload/secureAI/demos/tutorial-2-intent/Policies/user_bob.json +83 -0
- package/payload/secureAI/demos/tutorial-2-intent/README.md +213 -0
- package/payload/secureAI/demos/tutorial-2-intent/script.py +188 -0
- package/payload/secureAI/demos/tutorial-2-intent/setup/alation_verifier.py +177 -0
- package/payload/secureAI/demos/tutorial-2-intent/setup/get_alation_user_token.py +180 -0
- package/payload/secureAI/examples/README.md +244 -0
- package/payload/secureAI/examples/anthropic/anthropic_1a_dropin_simple.py +103 -0
- package/payload/secureAI/examples/anthropic/anthropic_1b_multiuser_bind.py +190 -0
- package/payload/secureAI/examples/anthropic/anthropic_1b_multiuser_bind_streaming.py +190 -0
- package/payload/secureAI/examples/anthropic/anthropic_1c_a2a_invoke.py +156 -0
- package/payload/secureAI/examples/attestations/1a_trade_alice.py +215 -0
- package/payload/secureAI/examples/attestations/1a_trade_bob.py +164 -0
- package/payload/secureAI/examples/attestations/1b_grant_admin.py +170 -0
- package/payload/secureAI/examples/attestations/1b_grant_researcher.py +231 -0
- package/payload/secureAI/examples/attestations/README.md +128 -0
- package/payload/secureAI/examples/langchain/langchain_1a_dropin_simple.py +199 -0
- package/payload/secureAI/examples/langchain/langchain_1b_multiuser.py +262 -0
- package/payload/secureAI/examples/langchain/langchain_1c_orchestration.py +332 -0
- package/payload/secureAI/examples/langchain/langchain_1d_llm_openai.py +130 -0
- package/payload/secureAI/examples/langchain/langchain_1e_llm_anthropic.py +130 -0
- package/payload/secureAI/examples/langchain/langchain_1f_memory.py +214 -0
- package/payload/secureAI/examples/litellm/litellm_1a_dropin_simple.py +131 -0
- package/payload/secureAI/examples/mcp/1a_simple_invocation.py +115 -0
- package/payload/secureAI/examples/mcp/1b_discovery_and_resources.py +142 -0
- package/payload/secureAI/examples/mcp/1c_logging_client.py +100 -0
- package/payload/secureAI/examples/mcp/1d_progress_client.py +100 -0
- package/payload/secureAI/examples/mcp/1e_sampling_client.py +150 -0
- package/payload/secureAI/examples/mcp/1e_sampling_server.py +90 -0
- package/payload/secureAI/examples/mcp/1f_elicitation_client.py +127 -0
- package/payload/secureAI/examples/mcp/1f_elicitation_server.py +73 -0
- package/payload/secureAI/examples/mcp/1g_roots_client.py +122 -0
- package/payload/secureAI/examples/mcp/1g_roots_server.py +85 -0
- package/payload/secureAI/examples/mcp/2a_proxy_snowflake.py +212 -0
- package/payload/secureAI/examples/mcp/2b_proxy_databricks.py +299 -0
- package/payload/secureAI/examples/mcp/2c_proxy_stdio_fetch.py +91 -0
- package/payload/secureAI/examples/mcp/2d_proxy_stdio_salesforce.py +119 -0
- package/payload/secureAI/examples/mcp/2e_proxy_multiuser.py +136 -0
- package/payload/secureAI/examples/mcp/2f_proxy_stdio_databricks.py +144 -0
- package/payload/secureAI/examples/mcp/3a_native_mcp_client.py +104 -0
- package/payload/secureAI/examples/mcp/3b_native_mcp_proxy.py +114 -0
- package/payload/secureAI/examples/mcp/3c_native_mcp_sampling.py +180 -0
- package/payload/secureAI/examples/mcp/3d_native_mcp_2026.py +135 -0
- package/payload/secureAI/examples/mcp/securemcp_calculator.py +248 -0
- package/payload/secureAI/examples/mcp/securemcp_server.py +75 -0
- package/payload/secureAI/examples/openai/openai_1a_dropin_simple.py +102 -0
- package/payload/secureAI/examples/openai/openai_1b_multiuser_bind.py +193 -0
- package/payload/secureAI/examples/openai/openai_1b_multiuser_bind_streaming.py +199 -0
- package/payload/secureAI/examples/openai/openai_1c_a2a_invoke.py +156 -0
- package/payload/secureAI/examples/openai/openai_1d_invocation_patterns.py +337 -0
- package/payload/secureAI/examples/phishing-resistance/1a_login_alice.py +180 -0
- package/payload/secureAI/examples/phishing-resistance/1b_trade_alice.py +241 -0
- package/payload/secureAI/examples/phishing-resistance/1b_trade_bob.py +164 -0
- package/payload/secureAI/examples/phishing-resistance/README.md +124 -0
- package/payload/secureAI/examples/phishing-resistance/get_token.py +114 -0
- package/payload/secureAI/examples/pydantic_ai/README.md +88 -0
- package/payload/secureAI/examples/pydantic_ai/policies/app_pydantic-agent.json +10 -0
- package/payload/secureAI/examples/pydantic_ai/pydanticai_1a_dropin_simple.py +90 -0
- package/payload/secureAI/examples/pydantic_ai/pydanticai_1b_model_routing.py +85 -0
- package/payload/secureAI/examples/pydantic_ai/pydanticai_1c_native_tools.py +83 -0
- package/payload/secureAI/examples/pydantic_ai/pydanticai_1d_multiuser_bind.py +104 -0
- package/payload/secureAI/examples/pydantic_ai/pydanticai_1e_mcp_compose.py +92 -0
- package/payload/secureAI/macaw_adapters/__init__.py +55 -0
- package/payload/secureAI/macaw_adapters/anthropic/REFERENCE.md +374 -0
- package/payload/secureAI/macaw_adapters/anthropic/__init__.py +22 -0
- package/payload/secureAI/macaw_adapters/anthropic/secure_anthropic.py +792 -0
- package/payload/secureAI/macaw_adapters/anthropic_ext_tools/__init__.py +25 -0
- package/payload/secureAI/macaw_adapters/anthropic_ext_tools/secure_anthropic.py +840 -0
- package/payload/secureAI/macaw_adapters/langchain/REFERENCE.md +234 -0
- package/payload/secureAI/macaw_adapters/langchain/__init__.py +100 -0
- package/payload/secureAI/macaw_adapters/langchain/_utils.py +83 -0
- package/payload/secureAI/macaw_adapters/langchain/agents.py +296 -0
- package/payload/secureAI/macaw_adapters/langchain/anthropic.py +456 -0
- package/payload/secureAI/macaw_adapters/langchain/callbacks.py +306 -0
- package/payload/secureAI/macaw_adapters/langchain/memory.py +352 -0
- package/payload/secureAI/macaw_adapters/langchain/openai.py +410 -0
- package/payload/secureAI/macaw_adapters/langchain/tools.py +174 -0
- package/payload/secureAI/macaw_adapters/litellm/README.md +216 -0
- package/payload/secureAI/macaw_adapters/litellm/__init__.py +143 -0
- package/payload/secureAI/macaw_adapters/litellm/secure_litellm.py +686 -0
- package/payload/secureAI/macaw_adapters/mcp/REFERENCE.md +460 -0
- package/payload/secureAI/macaw_adapters/mcp/__init__.py +65 -0
- package/payload/secureAI/macaw_adapters/mcp/_endpoint.py +193 -0
- package/payload/secureAI/macaw_adapters/mcp/_endpoint2.py +225 -0
- package/payload/secureAI/macaw_adapters/mcp/_serve.py +25 -0
- package/payload/secureAI/macaw_adapters/mcp/client.py +721 -0
- package/payload/secureAI/macaw_adapters/mcp/decorators.py +228 -0
- package/payload/secureAI/macaw_adapters/mcp/fastmcp_compat.py +353 -0
- package/payload/secureAI/macaw_adapters/mcp/mcp.py +785 -0
- package/payload/secureAI/macaw_adapters/mcp/proxy.py +626 -0
- package/payload/secureAI/macaw_adapters/mcp/server.py +317 -0
- package/payload/secureAI/macaw_adapters/mcp/subscriptions.py +450 -0
- package/payload/secureAI/macaw_adapters/openai/REFERENCE.md +368 -0
- package/payload/secureAI/macaw_adapters/openai/__init__.py +22 -0
- package/payload/secureAI/macaw_adapters/openai/secure_openai.py +833 -0
- package/payload/secureAI/macaw_adapters/openai_ext_tools/REFERENCE.md +368 -0
- package/payload/secureAI/macaw_adapters/openai_ext_tools/__init__.py +25 -0
- package/payload/secureAI/macaw_adapters/openai_ext_tools/secure_openai.py +876 -0
- package/payload/secureAI/macaw_adapters/pydantic_ai/__init__.py +36 -0
- package/payload/secureAI/macaw_adapters/pydantic_ai/_core.py +257 -0
- package/payload/secureAI/macaw_adapters/pydantic_ai/agent.py +218 -0
- package/payload/secureAI/macaw_adapters/pydantic_ai/model.py +99 -0
- package/payload/secureAI/macaw_adapters/pydantic_ai/toolset.py +86 -0
- package/payload/secureAI/test_harness_quick.py +296 -0
- package/wheels/macaw_adapters-0.9.10.1-py3-none-any.whl +0 -0
- package/wheels/macaw_client-0.9.10.1-cp312-cp312-macosx_11_0_arm64.whl +0 -0
- package/wheels/macaw_client-0.9.10.1-cp312-cp312-manylinux_2_17_x86_64.whl +0 -0
- package/wheels/macaw_client-0.9.10.1-cp314-cp314-macosx_11_0_arm64.whl +0 -0
- package/wheels/macaw_client-0.9.10.1-cp314-cp314-manylinux_2_17_x86_64.whl +0 -0
- package/wheels/macaw_client-0.9.10.1-cp314-cp314-win_amd64.whl +0 -0
- package/wheels/macaw_client-0.9.10.1-cp39-cp39-macosx_11_0_arm64.whl +0 -0
- package/README.md +0 -3
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MACAW Callback Handler - MACAW-integrated LangChain callbacks.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
from macaw_adapters.langchain.callbacks import MACAWCallbackHandler
|
|
6
|
+
|
|
7
|
+
# Simple usage with MACAWClient
|
|
8
|
+
handler = MACAWCallbackHandler.from_client(macaw_client)
|
|
9
|
+
chain = LLMChain(llm=llm, callbacks=[handler])
|
|
10
|
+
|
|
11
|
+
# Or with the drop-in ChatOpenAI
|
|
12
|
+
from macaw_adapters.langchain import ChatOpenAI, MACAWCallbackHandler
|
|
13
|
+
|
|
14
|
+
llm = ChatOpenAI(model="gpt-4")
|
|
15
|
+
handler = MACAWCallbackHandler.from_client(llm._macaw)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import logging
|
|
19
|
+
from typing import Any, Dict, List, Optional, Union
|
|
20
|
+
from uuid import UUID
|
|
21
|
+
|
|
22
|
+
from macaw_client import MACAWClient
|
|
23
|
+
from ._utils import get_or_create_client
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
from langchain_core.callbacks import BaseCallbackHandler
|
|
29
|
+
LANGCHAIN_AVAILABLE = True
|
|
30
|
+
except ImportError:
|
|
31
|
+
try:
|
|
32
|
+
from langchain.callbacks.base import BaseCallbackHandler
|
|
33
|
+
LANGCHAIN_AVAILABLE = True
|
|
34
|
+
except ImportError:
|
|
35
|
+
class BaseCallbackHandler:
|
|
36
|
+
pass
|
|
37
|
+
LANGCHAIN_AVAILABLE = False
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class MACAWCallbackHandler(BaseCallbackHandler):
|
|
41
|
+
"""
|
|
42
|
+
LangChain callback handler that logs all events through MACAW.
|
|
43
|
+
|
|
44
|
+
This provides audit logging for:
|
|
45
|
+
- LLM invocations (on_llm_start, on_llm_end, on_llm_error)
|
|
46
|
+
- Tool executions (on_tool_start, on_tool_end, on_tool_error)
|
|
47
|
+
- Chain executions (on_chain_start, on_chain_end, on_chain_error)
|
|
48
|
+
|
|
49
|
+
All events are logged via MACAWClient.log_event() for signed audit trails.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(self, macaw_client: MACAWClient):
|
|
53
|
+
"""
|
|
54
|
+
Initialize with a MACAWClient.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
macaw_client: MACAWClient instance for logging events
|
|
58
|
+
"""
|
|
59
|
+
if not LANGCHAIN_AVAILABLE:
|
|
60
|
+
raise ImportError("LangChain must be installed to use MACAWCallbackHandler")
|
|
61
|
+
|
|
62
|
+
super().__init__()
|
|
63
|
+
self._macaw = macaw_client
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def from_client(cls, macaw_client: MACAWClient) -> 'MACAWCallbackHandler':
|
|
67
|
+
"""Create handler from existing MACAWClient."""
|
|
68
|
+
return cls(macaw_client)
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def create(cls, app_name: str = "langchain-callbacks") -> 'MACAWCallbackHandler':
|
|
72
|
+
"""Create handler with a new MACAWClient."""
|
|
73
|
+
client = get_or_create_client(app_name)
|
|
74
|
+
if not client:
|
|
75
|
+
raise RuntimeError("Failed to create MACAWClient")
|
|
76
|
+
return cls(client)
|
|
77
|
+
|
|
78
|
+
# LLM Callbacks
|
|
79
|
+
|
|
80
|
+
def on_llm_start(
|
|
81
|
+
self,
|
|
82
|
+
serialized: Dict[str, Any],
|
|
83
|
+
prompts: List[str],
|
|
84
|
+
*,
|
|
85
|
+
run_id: UUID,
|
|
86
|
+
parent_run_id: Optional[UUID] = None,
|
|
87
|
+
**kwargs: Any
|
|
88
|
+
) -> None:
|
|
89
|
+
"""Log LLM invocation start."""
|
|
90
|
+
self._macaw.log_event(
|
|
91
|
+
event_type="llm_start",
|
|
92
|
+
action="start",
|
|
93
|
+
target=f"llm:{serialized.get('name', 'unknown')}",
|
|
94
|
+
metadata={
|
|
95
|
+
"run_id": str(run_id),
|
|
96
|
+
"parent_run_id": str(parent_run_id) if parent_run_id else None,
|
|
97
|
+
"model": serialized.get("name"),
|
|
98
|
+
"prompt_count": len(prompts),
|
|
99
|
+
"total_prompt_length": sum(len(p) for p in prompts)
|
|
100
|
+
}
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def on_llm_end(
|
|
104
|
+
self,
|
|
105
|
+
response: Any,
|
|
106
|
+
*,
|
|
107
|
+
run_id: UUID,
|
|
108
|
+
parent_run_id: Optional[UUID] = None,
|
|
109
|
+
**kwargs: Any
|
|
110
|
+
) -> None:
|
|
111
|
+
"""Log LLM invocation completion."""
|
|
112
|
+
self._macaw.log_event(
|
|
113
|
+
event_type="llm_end",
|
|
114
|
+
action="complete",
|
|
115
|
+
target="llm",
|
|
116
|
+
metadata={
|
|
117
|
+
"run_id": str(run_id),
|
|
118
|
+
"parent_run_id": str(parent_run_id) if parent_run_id else None
|
|
119
|
+
}
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
def on_llm_error(
|
|
123
|
+
self,
|
|
124
|
+
error: Union[Exception, KeyboardInterrupt],
|
|
125
|
+
*,
|
|
126
|
+
run_id: UUID,
|
|
127
|
+
parent_run_id: Optional[UUID] = None,
|
|
128
|
+
**kwargs: Any
|
|
129
|
+
) -> None:
|
|
130
|
+
"""Log LLM error."""
|
|
131
|
+
self._macaw.log_event(
|
|
132
|
+
event_type="llm_error",
|
|
133
|
+
action="error",
|
|
134
|
+
target="llm",
|
|
135
|
+
metadata={
|
|
136
|
+
"run_id": str(run_id),
|
|
137
|
+
"error": str(error)
|
|
138
|
+
}
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Tool Callbacks
|
|
142
|
+
|
|
143
|
+
def on_tool_start(
|
|
144
|
+
self,
|
|
145
|
+
serialized: Dict[str, Any],
|
|
146
|
+
input_str: str,
|
|
147
|
+
*,
|
|
148
|
+
run_id: UUID,
|
|
149
|
+
parent_run_id: Optional[UUID] = None,
|
|
150
|
+
**kwargs: Any
|
|
151
|
+
) -> None:
|
|
152
|
+
"""Log tool invocation start."""
|
|
153
|
+
tool_name = serialized.get("name", "unknown")
|
|
154
|
+
self._macaw.log_event(
|
|
155
|
+
event_type="tool_start",
|
|
156
|
+
action="start",
|
|
157
|
+
target=f"tool:{tool_name}",
|
|
158
|
+
metadata={
|
|
159
|
+
"run_id": str(run_id),
|
|
160
|
+
"parent_run_id": str(parent_run_id) if parent_run_id else None,
|
|
161
|
+
"tool": tool_name,
|
|
162
|
+
"input_length": len(input_str)
|
|
163
|
+
}
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
def on_tool_end(
|
|
167
|
+
self,
|
|
168
|
+
output: str,
|
|
169
|
+
*,
|
|
170
|
+
run_id: UUID,
|
|
171
|
+
parent_run_id: Optional[UUID] = None,
|
|
172
|
+
**kwargs: Any
|
|
173
|
+
) -> None:
|
|
174
|
+
"""Log tool completion."""
|
|
175
|
+
self._macaw.log_event(
|
|
176
|
+
event_type="tool_end",
|
|
177
|
+
action="complete",
|
|
178
|
+
target="tool",
|
|
179
|
+
metadata={
|
|
180
|
+
"run_id": str(run_id),
|
|
181
|
+
"output_length": len(str(output)) if output else 0
|
|
182
|
+
}
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
def on_tool_error(
|
|
186
|
+
self,
|
|
187
|
+
error: Union[Exception, KeyboardInterrupt],
|
|
188
|
+
*,
|
|
189
|
+
run_id: UUID,
|
|
190
|
+
parent_run_id: Optional[UUID] = None,
|
|
191
|
+
**kwargs: Any
|
|
192
|
+
) -> None:
|
|
193
|
+
"""Log tool error."""
|
|
194
|
+
self._macaw.log_event(
|
|
195
|
+
event_type="tool_error",
|
|
196
|
+
action="error",
|
|
197
|
+
target="tool",
|
|
198
|
+
metadata={
|
|
199
|
+
"run_id": str(run_id),
|
|
200
|
+
"error": str(error)
|
|
201
|
+
}
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
# Chain Callbacks
|
|
205
|
+
|
|
206
|
+
def on_chain_start(
|
|
207
|
+
self,
|
|
208
|
+
serialized: Dict[str, Any],
|
|
209
|
+
inputs: Dict[str, Any],
|
|
210
|
+
*,
|
|
211
|
+
run_id: UUID,
|
|
212
|
+
parent_run_id: Optional[UUID] = None,
|
|
213
|
+
**kwargs: Any
|
|
214
|
+
) -> None:
|
|
215
|
+
"""Log chain execution start."""
|
|
216
|
+
chain_name = serialized.get("name", "unknown")
|
|
217
|
+
self._macaw.log_event(
|
|
218
|
+
event_type="chain_start",
|
|
219
|
+
action="start",
|
|
220
|
+
target=f"chain:{chain_name}",
|
|
221
|
+
metadata={
|
|
222
|
+
"run_id": str(run_id),
|
|
223
|
+
"parent_run_id": str(parent_run_id) if parent_run_id else None,
|
|
224
|
+
"chain": chain_name,
|
|
225
|
+
"input_keys": list(inputs.keys())
|
|
226
|
+
}
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
def on_chain_end(
|
|
230
|
+
self,
|
|
231
|
+
outputs: Dict[str, Any],
|
|
232
|
+
*,
|
|
233
|
+
run_id: UUID,
|
|
234
|
+
parent_run_id: Optional[UUID] = None,
|
|
235
|
+
**kwargs: Any
|
|
236
|
+
) -> None:
|
|
237
|
+
"""Log chain completion."""
|
|
238
|
+
self._macaw.log_event(
|
|
239
|
+
event_type="chain_end",
|
|
240
|
+
action="complete",
|
|
241
|
+
target="chain",
|
|
242
|
+
metadata={
|
|
243
|
+
"run_id": str(run_id),
|
|
244
|
+
"output_keys": list(outputs.keys()) if outputs else []
|
|
245
|
+
}
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
def on_chain_error(
|
|
249
|
+
self,
|
|
250
|
+
error: Union[Exception, KeyboardInterrupt],
|
|
251
|
+
*,
|
|
252
|
+
run_id: UUID,
|
|
253
|
+
parent_run_id: Optional[UUID] = None,
|
|
254
|
+
**kwargs: Any
|
|
255
|
+
) -> None:
|
|
256
|
+
"""Log chain error."""
|
|
257
|
+
self._macaw.log_event(
|
|
258
|
+
event_type="chain_error",
|
|
259
|
+
action="error",
|
|
260
|
+
target="chain",
|
|
261
|
+
metadata={
|
|
262
|
+
"run_id": str(run_id),
|
|
263
|
+
"error": str(error)
|
|
264
|
+
}
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
# Agent Callbacks
|
|
268
|
+
|
|
269
|
+
def on_agent_action(
|
|
270
|
+
self,
|
|
271
|
+
action: Any,
|
|
272
|
+
*,
|
|
273
|
+
run_id: UUID,
|
|
274
|
+
parent_run_id: Optional[UUID] = None,
|
|
275
|
+
**kwargs: Any
|
|
276
|
+
) -> None:
|
|
277
|
+
"""Log agent action."""
|
|
278
|
+
self._macaw.log_event(
|
|
279
|
+
event_type="agent_action",
|
|
280
|
+
action="action",
|
|
281
|
+
target="agent",
|
|
282
|
+
metadata={
|
|
283
|
+
"run_id": str(run_id),
|
|
284
|
+
"tool": getattr(action, 'tool', 'unknown'),
|
|
285
|
+
"tool_input": str(getattr(action, 'tool_input', ''))[:100]
|
|
286
|
+
}
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
def on_agent_finish(
|
|
290
|
+
self,
|
|
291
|
+
finish: Any,
|
|
292
|
+
*,
|
|
293
|
+
run_id: UUID,
|
|
294
|
+
parent_run_id: Optional[UUID] = None,
|
|
295
|
+
**kwargs: Any
|
|
296
|
+
) -> None:
|
|
297
|
+
"""Log agent finish."""
|
|
298
|
+
self._macaw.log_event(
|
|
299
|
+
event_type="agent_finish",
|
|
300
|
+
action="complete",
|
|
301
|
+
target="agent",
|
|
302
|
+
metadata={
|
|
303
|
+
"run_id": str(run_id),
|
|
304
|
+
"return_values": list(getattr(finish, 'return_values', {}).keys())
|
|
305
|
+
}
|
|
306
|
+
)
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MACAW Memory Classes - Drop-in replacements for LangChain memory with MACAW context backing.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
# Instead of: from langchain.memory import ConversationBufferMemory
|
|
6
|
+
from macaw_adapters.langchain.memory import ConversationBufferMemory
|
|
7
|
+
|
|
8
|
+
# Same API, MACAW context vault backing
|
|
9
|
+
memory = ConversationBufferMemory()
|
|
10
|
+
chain = ConversationChain(llm=llm, memory=memory)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
import uuid
|
|
15
|
+
from typing import Any, Dict, List, Optional
|
|
16
|
+
|
|
17
|
+
from macaw_client import MACAWClient
|
|
18
|
+
from ._utils import get_or_create_client, cleanup_client
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ConversationBufferMemory:
|
|
24
|
+
"""
|
|
25
|
+
Drop-in replacement for LangChain's ConversationBufferMemory with MACAW backing.
|
|
26
|
+
|
|
27
|
+
This memory class stores conversation history in MACAW's secure context vault,
|
|
28
|
+
providing audit logging and secure storage for all memory operations.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
memory_key: str = "history",
|
|
34
|
+
input_key: str = "input",
|
|
35
|
+
output_key: str = "output",
|
|
36
|
+
human_prefix: str = "Human",
|
|
37
|
+
ai_prefix: str = "AI",
|
|
38
|
+
return_messages: bool = False,
|
|
39
|
+
session_id: Optional[str] = None,
|
|
40
|
+
macaw_client: Optional[MACAWClient] = None,
|
|
41
|
+
**kwargs
|
|
42
|
+
):
|
|
43
|
+
"""
|
|
44
|
+
Initialize SecureConversationBufferMemory.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
memory_key: Key to use for memory in chain context
|
|
48
|
+
input_key: Key for input in chain context
|
|
49
|
+
output_key: Key for output in chain context
|
|
50
|
+
human_prefix: Prefix for human messages
|
|
51
|
+
ai_prefix: Prefix for AI messages
|
|
52
|
+
return_messages: Return messages as list vs string
|
|
53
|
+
session_id: Unique session ID (auto-generated if not provided)
|
|
54
|
+
macaw_client: Optional pre-configured MACAWClient
|
|
55
|
+
**kwargs: Additional arguments
|
|
56
|
+
"""
|
|
57
|
+
self.memory_key = memory_key
|
|
58
|
+
self.input_key = input_key
|
|
59
|
+
self.output_key = output_key
|
|
60
|
+
self.human_prefix = human_prefix
|
|
61
|
+
self.ai_prefix = ai_prefix
|
|
62
|
+
self.return_messages = return_messages
|
|
63
|
+
self._kwargs = kwargs
|
|
64
|
+
|
|
65
|
+
# Session identification
|
|
66
|
+
self.session_id = session_id or str(uuid.uuid4())
|
|
67
|
+
self._context_key = f"memory:{self.session_id}"
|
|
68
|
+
|
|
69
|
+
# Initialize MACAW client
|
|
70
|
+
self._macaw = macaw_client or get_or_create_client("langchain-memory")
|
|
71
|
+
|
|
72
|
+
# Local buffer (synced with MACAW context)
|
|
73
|
+
self._buffer: List[Dict[str, str]] = []
|
|
74
|
+
|
|
75
|
+
# Load existing memory from context if available
|
|
76
|
+
self._load_from_context()
|
|
77
|
+
|
|
78
|
+
def _load_from_context(self) -> None:
|
|
79
|
+
"""Load existing memory from MACAW context."""
|
|
80
|
+
if self._macaw:
|
|
81
|
+
stored = self._macaw.context_get(self._context_key)
|
|
82
|
+
if stored and isinstance(stored, list):
|
|
83
|
+
self._buffer = stored
|
|
84
|
+
logger.debug(f"Loaded {len(self._buffer)} messages from context")
|
|
85
|
+
|
|
86
|
+
def _save_to_context(self) -> None:
|
|
87
|
+
"""Save current memory to MACAW context."""
|
|
88
|
+
if self._macaw:
|
|
89
|
+
self._macaw.context_set(self._context_key, self._buffer)
|
|
90
|
+
logger.debug(f"Saved {len(self._buffer)} messages to context")
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def buffer(self) -> List[Dict[str, str]]:
|
|
94
|
+
"""Get the conversation buffer."""
|
|
95
|
+
return self._buffer
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def memory_variables(self) -> List[str]:
|
|
99
|
+
"""Return memory variables."""
|
|
100
|
+
return [self.memory_key]
|
|
101
|
+
|
|
102
|
+
def load_memory_variables(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
|
103
|
+
"""
|
|
104
|
+
Load memory variables for chain context.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
inputs: Input dictionary (not used for buffer memory)
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Dictionary with memory_key containing conversation history
|
|
111
|
+
"""
|
|
112
|
+
# Reload from context to get latest
|
|
113
|
+
self._load_from_context()
|
|
114
|
+
|
|
115
|
+
if self.return_messages:
|
|
116
|
+
# Return as list of message objects
|
|
117
|
+
try:
|
|
118
|
+
from langchain_core.messages import HumanMessage, AIMessage
|
|
119
|
+
messages = []
|
|
120
|
+
for msg in self._buffer:
|
|
121
|
+
if msg.get("type") == "human":
|
|
122
|
+
messages.append(HumanMessage(content=msg["content"]))
|
|
123
|
+
else:
|
|
124
|
+
messages.append(AIMessage(content=msg["content"]))
|
|
125
|
+
return {self.memory_key: messages}
|
|
126
|
+
except ImportError:
|
|
127
|
+
# Fallback to dicts if langchain_core not available
|
|
128
|
+
return {self.memory_key: self._buffer}
|
|
129
|
+
else:
|
|
130
|
+
# Return as formatted string
|
|
131
|
+
lines = []
|
|
132
|
+
for msg in self._buffer:
|
|
133
|
+
prefix = self.human_prefix if msg.get("type") == "human" else self.ai_prefix
|
|
134
|
+
lines.append(f"{prefix}: {msg['content']}")
|
|
135
|
+
return {self.memory_key: "\n".join(lines)}
|
|
136
|
+
|
|
137
|
+
def save_context(self, inputs: Dict[str, Any], outputs: Dict[str, Any]) -> None:
|
|
138
|
+
"""
|
|
139
|
+
Save context from this conversation turn.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
inputs: Input dictionary with user message
|
|
143
|
+
outputs: Output dictionary with AI response
|
|
144
|
+
"""
|
|
145
|
+
# Extract input and output
|
|
146
|
+
input_str = inputs.get(self.input_key, "")
|
|
147
|
+
output_str = outputs.get(self.output_key, "")
|
|
148
|
+
|
|
149
|
+
# Handle different output formats
|
|
150
|
+
if hasattr(output_str, 'content'):
|
|
151
|
+
output_str = output_str.content
|
|
152
|
+
elif isinstance(output_str, dict):
|
|
153
|
+
output_str = output_str.get('content', str(output_str))
|
|
154
|
+
|
|
155
|
+
# Add to buffer
|
|
156
|
+
if input_str:
|
|
157
|
+
self._buffer.append({"type": "human", "content": str(input_str)})
|
|
158
|
+
if output_str:
|
|
159
|
+
self._buffer.append({"type": "ai", "content": str(output_str)})
|
|
160
|
+
|
|
161
|
+
# Persist to MACAW context
|
|
162
|
+
self._save_to_context()
|
|
163
|
+
|
|
164
|
+
def clear(self) -> None:
|
|
165
|
+
"""Clear memory contents."""
|
|
166
|
+
self._buffer = []
|
|
167
|
+
self._save_to_context()
|
|
168
|
+
logger.debug(f"Cleared memory for session {self.session_id}")
|
|
169
|
+
|
|
170
|
+
def add_user_message(self, message: str) -> None:
|
|
171
|
+
"""Add a user message to the buffer."""
|
|
172
|
+
self._buffer.append({"type": "human", "content": message})
|
|
173
|
+
self._save_to_context()
|
|
174
|
+
|
|
175
|
+
def add_ai_message(self, message: str) -> None:
|
|
176
|
+
"""Add an AI message to the buffer."""
|
|
177
|
+
self._buffer.append({"type": "ai", "content": message})
|
|
178
|
+
self._save_to_context()
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class ConversationBufferWindowMemory(ConversationBufferMemory):
|
|
182
|
+
"""
|
|
183
|
+
Buffer memory that only keeps the last k conversation turns.
|
|
184
|
+
|
|
185
|
+
Drop-in replacement for LangChain's ConversationBufferWindowMemory.
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
def __init__(self, k: int = 5, **kwargs):
|
|
189
|
+
"""
|
|
190
|
+
Initialize with window size.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
k: Number of conversation turns to keep
|
|
194
|
+
**kwargs: Arguments passed to ConversationBufferMemory
|
|
195
|
+
"""
|
|
196
|
+
super().__init__(**kwargs)
|
|
197
|
+
self.k = k
|
|
198
|
+
|
|
199
|
+
def load_memory_variables(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
|
200
|
+
"""Load only the last k turns."""
|
|
201
|
+
self._load_from_context()
|
|
202
|
+
|
|
203
|
+
# Keep only last k*2 messages (k turns = k human + k ai)
|
|
204
|
+
window = self._buffer[-(self.k * 2):]
|
|
205
|
+
|
|
206
|
+
if self.return_messages:
|
|
207
|
+
try:
|
|
208
|
+
from langchain_core.messages import HumanMessage, AIMessage
|
|
209
|
+
messages = []
|
|
210
|
+
for msg in window:
|
|
211
|
+
if msg.get("type") == "human":
|
|
212
|
+
messages.append(HumanMessage(content=msg["content"]))
|
|
213
|
+
else:
|
|
214
|
+
messages.append(AIMessage(content=msg["content"]))
|
|
215
|
+
return {self.memory_key: messages}
|
|
216
|
+
except ImportError:
|
|
217
|
+
return {self.memory_key: window}
|
|
218
|
+
else:
|
|
219
|
+
lines = []
|
|
220
|
+
for msg in window:
|
|
221
|
+
prefix = self.human_prefix if msg.get("type") == "human" else self.ai_prefix
|
|
222
|
+
lines.append(f"{prefix}: {msg['content']}")
|
|
223
|
+
return {self.memory_key: "\n".join(lines)}
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class ConversationSummaryMemory:
|
|
227
|
+
"""
|
|
228
|
+
Memory that summarizes conversation history.
|
|
229
|
+
|
|
230
|
+
Drop-in replacement for LangChain's ConversationSummaryMemory.
|
|
231
|
+
Stores summary in MACAW context vault.
|
|
232
|
+
"""
|
|
233
|
+
|
|
234
|
+
def __init__(
|
|
235
|
+
self,
|
|
236
|
+
llm: Any = None,
|
|
237
|
+
memory_key: str = "history",
|
|
238
|
+
human_prefix: str = "Human",
|
|
239
|
+
ai_prefix: str = "AI",
|
|
240
|
+
session_id: Optional[str] = None,
|
|
241
|
+
macaw_client: Optional[MACAWClient] = None,
|
|
242
|
+
**kwargs
|
|
243
|
+
):
|
|
244
|
+
"""
|
|
245
|
+
Initialize summary memory.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
llm: Language model for summarization
|
|
249
|
+
memory_key: Key for memory in chain context
|
|
250
|
+
human_prefix: Prefix for human messages
|
|
251
|
+
ai_prefix: Prefix for AI messages
|
|
252
|
+
session_id: Unique session ID
|
|
253
|
+
macaw_client: Optional pre-configured MACAWClient
|
|
254
|
+
"""
|
|
255
|
+
self.llm = llm
|
|
256
|
+
self.memory_key = memory_key
|
|
257
|
+
self.human_prefix = human_prefix
|
|
258
|
+
self.ai_prefix = ai_prefix
|
|
259
|
+
self._kwargs = kwargs
|
|
260
|
+
|
|
261
|
+
# Session identification
|
|
262
|
+
self.session_id = session_id or str(uuid.uuid4())
|
|
263
|
+
self._summary_key = f"memory_summary:{self.session_id}"
|
|
264
|
+
self._buffer_key = f"memory_buffer:{self.session_id}"
|
|
265
|
+
|
|
266
|
+
# Initialize MACAW client
|
|
267
|
+
self._macaw = macaw_client or get_or_create_client("langchain-memory")
|
|
268
|
+
|
|
269
|
+
# Local state
|
|
270
|
+
self._summary = ""
|
|
271
|
+
self._buffer: List[Dict[str, str]] = []
|
|
272
|
+
|
|
273
|
+
# Load existing from context
|
|
274
|
+
self._load_from_context()
|
|
275
|
+
|
|
276
|
+
def _load_from_context(self) -> None:
|
|
277
|
+
"""Load existing summary and buffer from context."""
|
|
278
|
+
if self._macaw:
|
|
279
|
+
summary = self._macaw.context_get(self._summary_key)
|
|
280
|
+
if summary:
|
|
281
|
+
self._summary = summary
|
|
282
|
+
|
|
283
|
+
buffer = self._macaw.context_get(self._buffer_key)
|
|
284
|
+
if buffer and isinstance(buffer, list):
|
|
285
|
+
self._buffer = buffer
|
|
286
|
+
|
|
287
|
+
def _save_to_context(self) -> None:
|
|
288
|
+
"""Save summary and buffer to context."""
|
|
289
|
+
if self._macaw:
|
|
290
|
+
self._macaw.context_set(self._summary_key, self._summary)
|
|
291
|
+
self._macaw.context_set(self._buffer_key, self._buffer)
|
|
292
|
+
|
|
293
|
+
@property
|
|
294
|
+
def memory_variables(self) -> List[str]:
|
|
295
|
+
return [self.memory_key]
|
|
296
|
+
|
|
297
|
+
def load_memory_variables(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
|
298
|
+
"""Load summary as memory variable."""
|
|
299
|
+
self._load_from_context()
|
|
300
|
+
return {self.memory_key: self._summary}
|
|
301
|
+
|
|
302
|
+
def save_context(self, inputs: Dict[str, Any], outputs: Dict[str, Any]) -> None:
|
|
303
|
+
"""Save context and update summary."""
|
|
304
|
+
input_key = self._kwargs.get('input_key', 'input')
|
|
305
|
+
output_key = self._kwargs.get('output_key', 'output')
|
|
306
|
+
|
|
307
|
+
input_str = inputs.get(input_key, "")
|
|
308
|
+
output_str = outputs.get(output_key, "")
|
|
309
|
+
|
|
310
|
+
if hasattr(output_str, 'content'):
|
|
311
|
+
output_str = output_str.content
|
|
312
|
+
|
|
313
|
+
# Add to buffer
|
|
314
|
+
new_lines = f"{self.human_prefix}: {input_str}\n{self.ai_prefix}: {output_str}"
|
|
315
|
+
self._buffer.append({"input": input_str, "output": output_str})
|
|
316
|
+
|
|
317
|
+
# Update summary using LLM if available
|
|
318
|
+
if self.llm:
|
|
319
|
+
try:
|
|
320
|
+
prompt = f"""Progressively summarize the conversation, adding to the previous summary.
|
|
321
|
+
|
|
322
|
+
Current summary: {self._summary}
|
|
323
|
+
|
|
324
|
+
New lines of conversation:
|
|
325
|
+
{new_lines}
|
|
326
|
+
|
|
327
|
+
New summary:"""
|
|
328
|
+
response = self.llm.invoke(prompt)
|
|
329
|
+
if hasattr(response, 'content'):
|
|
330
|
+
self._summary = response.content
|
|
331
|
+
else:
|
|
332
|
+
self._summary = str(response)
|
|
333
|
+
except Exception as e:
|
|
334
|
+
logger.warning(f"Failed to update summary: {e}")
|
|
335
|
+
# Fallback: append to summary
|
|
336
|
+
self._summary = f"{self._summary}\n{new_lines}" if self._summary else new_lines
|
|
337
|
+
else:
|
|
338
|
+
# No LLM - just concatenate
|
|
339
|
+
self._summary = f"{self._summary}\n{new_lines}" if self._summary else new_lines
|
|
340
|
+
|
|
341
|
+
self._save_to_context()
|
|
342
|
+
|
|
343
|
+
def clear(self) -> None:
|
|
344
|
+
"""Clear memory."""
|
|
345
|
+
self._summary = ""
|
|
346
|
+
self._buffer = []
|
|
347
|
+
self._save_to_context()
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def cleanup():
|
|
351
|
+
"""Clean up the langchain-memory MACAW client."""
|
|
352
|
+
cleanup_client("langchain-memory")
|