@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.
Files changed (142) hide show
  1. package/LICENSE.md +339 -0
  2. package/bin/install.js +278 -0
  3. package/package.json +37 -4
  4. package/payload/README.md +56 -0
  5. package/payload/secureAI/LICENSE +190 -0
  6. package/payload/secureAI/NOTICE +5 -0
  7. package/payload/secureAI/demos/policy-controls/1_uncontrolled.py +139 -0
  8. package/payload/secureAI/demos/policy-controls/2_with_macaw.py +139 -0
  9. package/payload/secureAI/demos/policy-controls/3_with_user_policies.py +162 -0
  10. package/payload/secureAI/demos/policy-controls/credentials.txt +12 -0
  11. package/payload/secureAI/demos/policy-controls/malicious_report.txt +21 -0
  12. package/payload/secureAI/demos/policy-controls/sales_report.txt +20 -0
  13. package/payload/secureAI/demos/tutorial-1-rbac/README.md +143 -0
  14. package/payload/secureAI/demos/tutorial-1-rbac/app.py +147 -0
  15. package/payload/secureAI/demos/tutorial-1-rbac/authprompts_demo.py +306 -0
  16. package/payload/secureAI/demos/tutorial-1-rbac/config/claims-config.yaml +152 -0
  17. package/payload/secureAI/demos/tutorial-1-rbac/demo_secureopenai_with_policies.py +150 -0
  18. package/payload/secureAI/demos/tutorial-1-rbac/policies/bu_Analytics.json +8 -0
  19. package/payload/secureAI/demos/tutorial-1-rbac/policies/company_FinTech_Corp.json +70 -0
  20. package/payload/secureAI/demos/tutorial-1-rbac/policies/llm-base.json +69 -0
  21. package/payload/secureAI/demos/tutorial-1-rbac/policies/team_Reporting.json +8 -0
  22. package/payload/secureAI/demos/tutorial-1-rbac/policies/user_alice.json +32 -0
  23. package/payload/secureAI/demos/tutorial-1-rbac/policies/user_bob.json +28 -0
  24. package/payload/secureAI/demos/tutorial-1-rbac/policies/user_carol.json +34 -0
  25. package/payload/secureAI/demos/tutorial-1-rbac/requirements.txt +6 -0
  26. package/payload/secureAI/demos/tutorial-1-rbac/setup/auth0_complete_setup.sh +392 -0
  27. package/payload/secureAI/demos/tutorial-1-rbac/setup/keycloak_complete_setup.sh +320 -0
  28. package/payload/secureAI/demos/tutorial-2-intent/Policies/alation-remote-proxy.json +155 -0
  29. package/payload/secureAI/demos/tutorial-2-intent/Policies/bu.json +22 -0
  30. package/payload/secureAI/demos/tutorial-2-intent/Policies/company.json +17 -0
  31. package/payload/secureAI/demos/tutorial-2-intent/Policies/user_alice.json +48 -0
  32. package/payload/secureAI/demos/tutorial-2-intent/Policies/user_bob.json +83 -0
  33. package/payload/secureAI/demos/tutorial-2-intent/README.md +213 -0
  34. package/payload/secureAI/demos/tutorial-2-intent/script.py +188 -0
  35. package/payload/secureAI/demos/tutorial-2-intent/setup/alation_verifier.py +177 -0
  36. package/payload/secureAI/demos/tutorial-2-intent/setup/get_alation_user_token.py +180 -0
  37. package/payload/secureAI/examples/README.md +244 -0
  38. package/payload/secureAI/examples/anthropic/anthropic_1a_dropin_simple.py +103 -0
  39. package/payload/secureAI/examples/anthropic/anthropic_1b_multiuser_bind.py +190 -0
  40. package/payload/secureAI/examples/anthropic/anthropic_1b_multiuser_bind_streaming.py +190 -0
  41. package/payload/secureAI/examples/anthropic/anthropic_1c_a2a_invoke.py +156 -0
  42. package/payload/secureAI/examples/attestations/1a_trade_alice.py +215 -0
  43. package/payload/secureAI/examples/attestations/1a_trade_bob.py +164 -0
  44. package/payload/secureAI/examples/attestations/1b_grant_admin.py +170 -0
  45. package/payload/secureAI/examples/attestations/1b_grant_researcher.py +231 -0
  46. package/payload/secureAI/examples/attestations/README.md +128 -0
  47. package/payload/secureAI/examples/langchain/langchain_1a_dropin_simple.py +199 -0
  48. package/payload/secureAI/examples/langchain/langchain_1b_multiuser.py +262 -0
  49. package/payload/secureAI/examples/langchain/langchain_1c_orchestration.py +332 -0
  50. package/payload/secureAI/examples/langchain/langchain_1d_llm_openai.py +130 -0
  51. package/payload/secureAI/examples/langchain/langchain_1e_llm_anthropic.py +130 -0
  52. package/payload/secureAI/examples/langchain/langchain_1f_memory.py +214 -0
  53. package/payload/secureAI/examples/litellm/litellm_1a_dropin_simple.py +131 -0
  54. package/payload/secureAI/examples/mcp/1a_simple_invocation.py +115 -0
  55. package/payload/secureAI/examples/mcp/1b_discovery_and_resources.py +142 -0
  56. package/payload/secureAI/examples/mcp/1c_logging_client.py +100 -0
  57. package/payload/secureAI/examples/mcp/1d_progress_client.py +100 -0
  58. package/payload/secureAI/examples/mcp/1e_sampling_client.py +150 -0
  59. package/payload/secureAI/examples/mcp/1e_sampling_server.py +90 -0
  60. package/payload/secureAI/examples/mcp/1f_elicitation_client.py +127 -0
  61. package/payload/secureAI/examples/mcp/1f_elicitation_server.py +73 -0
  62. package/payload/secureAI/examples/mcp/1g_roots_client.py +122 -0
  63. package/payload/secureAI/examples/mcp/1g_roots_server.py +85 -0
  64. package/payload/secureAI/examples/mcp/2a_proxy_snowflake.py +212 -0
  65. package/payload/secureAI/examples/mcp/2b_proxy_databricks.py +299 -0
  66. package/payload/secureAI/examples/mcp/2c_proxy_stdio_fetch.py +91 -0
  67. package/payload/secureAI/examples/mcp/2d_proxy_stdio_salesforce.py +119 -0
  68. package/payload/secureAI/examples/mcp/2e_proxy_multiuser.py +136 -0
  69. package/payload/secureAI/examples/mcp/2f_proxy_stdio_databricks.py +144 -0
  70. package/payload/secureAI/examples/mcp/3a_native_mcp_client.py +104 -0
  71. package/payload/secureAI/examples/mcp/3b_native_mcp_proxy.py +114 -0
  72. package/payload/secureAI/examples/mcp/3c_native_mcp_sampling.py +180 -0
  73. package/payload/secureAI/examples/mcp/3d_native_mcp_2026.py +135 -0
  74. package/payload/secureAI/examples/mcp/securemcp_calculator.py +248 -0
  75. package/payload/secureAI/examples/mcp/securemcp_server.py +75 -0
  76. package/payload/secureAI/examples/openai/openai_1a_dropin_simple.py +102 -0
  77. package/payload/secureAI/examples/openai/openai_1b_multiuser_bind.py +193 -0
  78. package/payload/secureAI/examples/openai/openai_1b_multiuser_bind_streaming.py +199 -0
  79. package/payload/secureAI/examples/openai/openai_1c_a2a_invoke.py +156 -0
  80. package/payload/secureAI/examples/openai/openai_1d_invocation_patterns.py +337 -0
  81. package/payload/secureAI/examples/phishing-resistance/1a_login_alice.py +180 -0
  82. package/payload/secureAI/examples/phishing-resistance/1b_trade_alice.py +241 -0
  83. package/payload/secureAI/examples/phishing-resistance/1b_trade_bob.py +164 -0
  84. package/payload/secureAI/examples/phishing-resistance/README.md +124 -0
  85. package/payload/secureAI/examples/phishing-resistance/get_token.py +114 -0
  86. package/payload/secureAI/examples/pydantic_ai/README.md +88 -0
  87. package/payload/secureAI/examples/pydantic_ai/policies/app_pydantic-agent.json +10 -0
  88. package/payload/secureAI/examples/pydantic_ai/pydanticai_1a_dropin_simple.py +90 -0
  89. package/payload/secureAI/examples/pydantic_ai/pydanticai_1b_model_routing.py +85 -0
  90. package/payload/secureAI/examples/pydantic_ai/pydanticai_1c_native_tools.py +83 -0
  91. package/payload/secureAI/examples/pydantic_ai/pydanticai_1d_multiuser_bind.py +104 -0
  92. package/payload/secureAI/examples/pydantic_ai/pydanticai_1e_mcp_compose.py +92 -0
  93. package/payload/secureAI/macaw_adapters/__init__.py +55 -0
  94. package/payload/secureAI/macaw_adapters/anthropic/REFERENCE.md +374 -0
  95. package/payload/secureAI/macaw_adapters/anthropic/__init__.py +22 -0
  96. package/payload/secureAI/macaw_adapters/anthropic/secure_anthropic.py +792 -0
  97. package/payload/secureAI/macaw_adapters/anthropic_ext_tools/__init__.py +25 -0
  98. package/payload/secureAI/macaw_adapters/anthropic_ext_tools/secure_anthropic.py +840 -0
  99. package/payload/secureAI/macaw_adapters/langchain/REFERENCE.md +234 -0
  100. package/payload/secureAI/macaw_adapters/langchain/__init__.py +100 -0
  101. package/payload/secureAI/macaw_adapters/langchain/_utils.py +83 -0
  102. package/payload/secureAI/macaw_adapters/langchain/agents.py +296 -0
  103. package/payload/secureAI/macaw_adapters/langchain/anthropic.py +456 -0
  104. package/payload/secureAI/macaw_adapters/langchain/callbacks.py +306 -0
  105. package/payload/secureAI/macaw_adapters/langchain/memory.py +352 -0
  106. package/payload/secureAI/macaw_adapters/langchain/openai.py +410 -0
  107. package/payload/secureAI/macaw_adapters/langchain/tools.py +174 -0
  108. package/payload/secureAI/macaw_adapters/litellm/README.md +216 -0
  109. package/payload/secureAI/macaw_adapters/litellm/__init__.py +143 -0
  110. package/payload/secureAI/macaw_adapters/litellm/secure_litellm.py +686 -0
  111. package/payload/secureAI/macaw_adapters/mcp/REFERENCE.md +460 -0
  112. package/payload/secureAI/macaw_adapters/mcp/__init__.py +65 -0
  113. package/payload/secureAI/macaw_adapters/mcp/_endpoint.py +193 -0
  114. package/payload/secureAI/macaw_adapters/mcp/_endpoint2.py +225 -0
  115. package/payload/secureAI/macaw_adapters/mcp/_serve.py +25 -0
  116. package/payload/secureAI/macaw_adapters/mcp/client.py +721 -0
  117. package/payload/secureAI/macaw_adapters/mcp/decorators.py +228 -0
  118. package/payload/secureAI/macaw_adapters/mcp/fastmcp_compat.py +353 -0
  119. package/payload/secureAI/macaw_adapters/mcp/mcp.py +785 -0
  120. package/payload/secureAI/macaw_adapters/mcp/proxy.py +626 -0
  121. package/payload/secureAI/macaw_adapters/mcp/server.py +317 -0
  122. package/payload/secureAI/macaw_adapters/mcp/subscriptions.py +450 -0
  123. package/payload/secureAI/macaw_adapters/openai/REFERENCE.md +368 -0
  124. package/payload/secureAI/macaw_adapters/openai/__init__.py +22 -0
  125. package/payload/secureAI/macaw_adapters/openai/secure_openai.py +833 -0
  126. package/payload/secureAI/macaw_adapters/openai_ext_tools/REFERENCE.md +368 -0
  127. package/payload/secureAI/macaw_adapters/openai_ext_tools/__init__.py +25 -0
  128. package/payload/secureAI/macaw_adapters/openai_ext_tools/secure_openai.py +876 -0
  129. package/payload/secureAI/macaw_adapters/pydantic_ai/__init__.py +36 -0
  130. package/payload/secureAI/macaw_adapters/pydantic_ai/_core.py +257 -0
  131. package/payload/secureAI/macaw_adapters/pydantic_ai/agent.py +218 -0
  132. package/payload/secureAI/macaw_adapters/pydantic_ai/model.py +99 -0
  133. package/payload/secureAI/macaw_adapters/pydantic_ai/toolset.py +86 -0
  134. package/payload/secureAI/test_harness_quick.py +296 -0
  135. package/wheels/macaw_adapters-0.9.10.1-py3-none-any.whl +0 -0
  136. package/wheels/macaw_client-0.9.10.1-cp312-cp312-macosx_11_0_arm64.whl +0 -0
  137. package/wheels/macaw_client-0.9.10.1-cp312-cp312-manylinux_2_17_x86_64.whl +0 -0
  138. package/wheels/macaw_client-0.9.10.1-cp314-cp314-macosx_11_0_arm64.whl +0 -0
  139. package/wheels/macaw_client-0.9.10.1-cp314-cp314-manylinux_2_17_x86_64.whl +0 -0
  140. package/wheels/macaw_client-0.9.10.1-cp314-cp314-win_amd64.whl +0 -0
  141. package/wheels/macaw_client-0.9.10.1-cp39-cp39-macosx_11_0_arm64.whl +0 -0
  142. package/README.md +0 -3
@@ -0,0 +1,456 @@
1
+ """
2
+ MACAW ChatAnthropic - Drop-in replacement for langchain_anthropic.ChatAnthropic with MACAW protection.
3
+
4
+ Usage:
5
+ # Instead of: from langchain_anthropic import ChatAnthropic
6
+ from macaw_adapters.langchain.anthropic import ChatAnthropic
7
+
8
+ # Same API, MACAW security is invisible
9
+ llm = ChatAnthropic(model="claude-opus-4-8")
10
+ response = llm.invoke("Hello, world!")
11
+
12
+ Security:
13
+ This adapter uses SecureAnthropic internally, which routes all LLM calls through
14
+ MACAW's Policy Enforcement Point (PEP) via invoke_tool. This ensures:
15
+ - Policy enforcement on every LLM call
16
+ - Cryptographic audit logging
17
+ - Per-user identity propagation via bind_to_user
18
+ """
19
+
20
+ import logging
21
+ from typing import Any, Dict, Iterator, List, Optional
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+ # Global registry of ChatAnthropic instances for cleanup
26
+ _instances: List['ChatAnthropic'] = []
27
+
28
+
29
+ class ChatAnthropic:
30
+ """
31
+ Drop-in replacement for langchain_anthropic.ChatAnthropic with MACAW protection.
32
+
33
+ Uses SecureAnthropic internally to route all LLM calls through MACAW's PEP.
34
+ This ensures policy enforcement, not just logging.
35
+
36
+ MACAW is completely invisible - use exactly like native LangChain.
37
+ """
38
+
39
+ def __init__(
40
+ self,
41
+ model: str = "claude-opus-4-8",
42
+ temperature: Optional[float] = None,
43
+ max_tokens: int = 1024,
44
+ api_key: Optional[str] = None,
45
+ base_url: Optional[str] = None,
46
+ timeout: Optional[float] = None,
47
+ max_retries: int = 2,
48
+ **kwargs
49
+ ):
50
+ """
51
+ Initialize SecureChatAnthropic.
52
+
53
+ Args:
54
+ model: Anthropic model name (e.g., "claude-opus-4-8", "claude-sonnet-5")
55
+ temperature: Sampling temperature (0.0 to 1.0). Unset by default:
56
+ current Claude models reject sampling parameters, and the
57
+ `is not None` guards below skip it when unset.
58
+ max_tokens: Maximum tokens to generate
59
+ api_key: Anthropic API key (or use ANTHROPIC_API_KEY env var)
60
+ base_url: Custom API base URL
61
+ timeout: Request timeout in seconds
62
+ max_retries: Maximum retry attempts
63
+ **kwargs: Additional arguments
64
+ """
65
+ # Store configuration
66
+ self.model_name = model
67
+ self.temperature = temperature
68
+ self.max_tokens = max_tokens
69
+ self._kwargs = kwargs
70
+
71
+ # Store optional config for reference (not passed to SecureAnthropic)
72
+ self._base_url = base_url
73
+ self._timeout = timeout
74
+ self._max_retries = max_retries
75
+
76
+ # Create SecureAnthropic internally - it handles its own MACAWClient
77
+ # SecureAnthropic accepts: api_key, app_name, intent_policy
78
+ from macaw_adapters.anthropic import SecureAnthropic
79
+
80
+ self._secure_anthropic = SecureAnthropic(
81
+ app_name="langchain-anthropic",
82
+ api_key=api_key
83
+ )
84
+
85
+ # Track for cleanup
86
+ _instances.append(self)
87
+
88
+ logger.debug(f"[ChatAnthropic] Created with SecureAnthropic backend, model={model}")
89
+
90
+ def _to_anthropic_messages(self, input_data: Any) -> List[Dict[str, str]]:
91
+ """Convert LangChain input to Anthropic message format."""
92
+ if isinstance(input_data, str):
93
+ return [{"role": "user", "content": input_data}]
94
+
95
+ if isinstance(input_data, list):
96
+ messages = []
97
+ for msg in input_data:
98
+ if hasattr(msg, 'type') and hasattr(msg, 'content'):
99
+ # LangChain message object (HumanMessage, AIMessage, etc.)
100
+ # Anthropic uses "user" and "assistant" roles
101
+ role_map = {
102
+ "human": "user",
103
+ "ai": "assistant",
104
+ "system": "user", # Anthropic handles system via system param
105
+ "function": "user",
106
+ "tool": "user"
107
+ }
108
+ role = role_map.get(msg.type, "user")
109
+ messages.append({"role": role, "content": str(msg.content)})
110
+ elif isinstance(msg, dict):
111
+ # Already in dict format
112
+ messages.append(msg)
113
+ else:
114
+ # Fallback: treat as user message
115
+ messages.append({"role": "user", "content": str(msg)})
116
+ return messages
117
+
118
+ # Fallback: single user message
119
+ return [{"role": "user", "content": str(input_data)}]
120
+
121
+ def _extract_system_message(self, input_data: Any) -> Optional[str]:
122
+ """Extract system message from input if present."""
123
+ if isinstance(input_data, list):
124
+ for msg in input_data:
125
+ if hasattr(msg, 'type') and msg.type == "system":
126
+ return str(msg.content)
127
+ return None
128
+
129
+ def _to_langchain_message(self, anthropic_response: Any) -> Any:
130
+ """Convert Anthropic Message to LangChain AIMessage."""
131
+ try:
132
+ from langchain_core.messages import AIMessage
133
+
134
+ # Handle dict response
135
+ if isinstance(anthropic_response, dict):
136
+ content_blocks = anthropic_response.get("content", [])
137
+ if content_blocks:
138
+ # Concatenate text blocks
139
+ text_parts = []
140
+ for block in content_blocks:
141
+ if isinstance(block, dict) and block.get("type") == "text":
142
+ text_parts.append(block.get("text", ""))
143
+ elif isinstance(block, str):
144
+ text_parts.append(block)
145
+ return AIMessage(content="".join(text_parts))
146
+ return AIMessage(content="")
147
+
148
+ # Handle Message object
149
+ if hasattr(anthropic_response, 'content'):
150
+ content = anthropic_response.content
151
+ if isinstance(content, list):
152
+ # Content blocks
153
+ text_parts = []
154
+ for block in content:
155
+ if hasattr(block, 'text'):
156
+ text_parts.append(block.text)
157
+ elif hasattr(block, 'type') and block.type == "text":
158
+ text_parts.append(getattr(block, 'text', ''))
159
+ return AIMessage(content="".join(text_parts))
160
+ return AIMessage(content=str(content))
161
+
162
+ return AIMessage(content=str(anthropic_response))
163
+
164
+ except ImportError:
165
+ logger.warning("langchain_core not installed, returning raw response")
166
+ return anthropic_response
167
+
168
+ def _to_langchain_chunk(self, anthropic_event: Any) -> Any:
169
+ """Convert Anthropic streaming event to LangChain AIMessageChunk."""
170
+ try:
171
+ from langchain_core.messages import AIMessageChunk
172
+
173
+ # Handle different event types from Anthropic streaming
174
+ if hasattr(anthropic_event, 'type'):
175
+ if anthropic_event.type == "content_block_delta":
176
+ delta = getattr(anthropic_event, 'delta', None)
177
+ if delta and hasattr(delta, 'text'):
178
+ return AIMessageChunk(content=delta.text)
179
+ elif anthropic_event.type == "text":
180
+ return AIMessageChunk(content=getattr(anthropic_event, 'text', ''))
181
+
182
+ # Handle dict event
183
+ if isinstance(anthropic_event, dict):
184
+ if anthropic_event.get("type") == "content_block_delta":
185
+ delta = anthropic_event.get("delta", {})
186
+ return AIMessageChunk(content=delta.get("text", ""))
187
+ elif "text" in anthropic_event:
188
+ return AIMessageChunk(content=anthropic_event["text"])
189
+
190
+ return AIMessageChunk(content="")
191
+
192
+ except ImportError:
193
+ return anthropic_event
194
+
195
+ def invoke(self, input: Any, config: Optional[Dict] = None, **kwargs) -> Any:
196
+ """
197
+ Invoke the LLM with MACAW protection.
198
+
199
+ Args:
200
+ input: The input prompt (string or list of messages)
201
+ config: Optional configuration dict
202
+ **kwargs: Additional arguments
203
+
204
+ Returns:
205
+ LLM response (AIMessage)
206
+ """
207
+ # Convert to Anthropic format
208
+ messages = self._to_anthropic_messages(input)
209
+ system_msg = self._extract_system_message(input)
210
+
211
+ # Filter out system messages from messages list (handled separately)
212
+ if system_msg:
213
+ messages = [m for m in messages if m.get("role") != "user" or m.get("content") != system_msg]
214
+
215
+ # Build params
216
+ params = {
217
+ "model": self.model_name,
218
+ "messages": messages,
219
+ "max_tokens": self.max_tokens,
220
+ }
221
+ if self.temperature is not None:
222
+ params["temperature"] = self.temperature
223
+ if system_msg:
224
+ params["system"] = system_msg
225
+
226
+ # Merge additional kwargs
227
+ params.update(kwargs)
228
+
229
+ # Call SecureAnthropic (routes through PEP!)
230
+ response = self._secure_anthropic.messages.create(**params)
231
+
232
+ # Convert to LangChain format
233
+ return self._to_langchain_message(response)
234
+
235
+ def stream(self, input: Any, config: Optional[Dict] = None, **kwargs) -> Iterator:
236
+ """
237
+ Stream responses from the LLM with MACAW protection.
238
+
239
+ Args:
240
+ input: The input prompt
241
+ config: Optional configuration dict
242
+ **kwargs: Additional arguments
243
+
244
+ Yields:
245
+ Streaming chunks (AIMessageChunk)
246
+ """
247
+ # Convert to Anthropic format
248
+ messages = self._to_anthropic_messages(input)
249
+ system_msg = self._extract_system_message(input)
250
+
251
+ if system_msg:
252
+ messages = [m for m in messages if m.get("role") != "user" or m.get("content") != system_msg]
253
+
254
+ # Build params
255
+ params = {
256
+ "model": self.model_name,
257
+ "messages": messages,
258
+ "max_tokens": self.max_tokens,
259
+ }
260
+ if self.temperature is not None:
261
+ params["temperature"] = self.temperature
262
+ if system_msg:
263
+ params["system"] = system_msg
264
+
265
+ params.update(kwargs)
266
+
267
+ # Stream from SecureAnthropic (routes through PEP!)
268
+ with self._secure_anthropic.messages.stream(**params) as stream:
269
+ for event in stream:
270
+ chunk = self._to_langchain_chunk(event)
271
+ if chunk:
272
+ yield chunk
273
+
274
+ def batch(self, inputs: List[Any], config: Optional[Dict] = None, **kwargs) -> List:
275
+ """
276
+ Batch invoke the LLM with MACAW protection.
277
+
278
+ Args:
279
+ inputs: List of input prompts
280
+ config: Optional configuration dict
281
+ **kwargs: Additional arguments
282
+
283
+ Returns:
284
+ List of LLM responses (AIMessage)
285
+ """
286
+ return [self.invoke(input, config=config, **kwargs) for input in inputs]
287
+
288
+ async def ainvoke(self, input: Any, config: Optional[Dict] = None, **kwargs) -> Any:
289
+ """Async invoke with MACAW protection."""
290
+ return self.invoke(input, config=config, **kwargs)
291
+
292
+ async def astream(self, input: Any, config: Optional[Dict] = None, **kwargs):
293
+ """Async stream with MACAW protection."""
294
+ for chunk in self.stream(input, config=config, **kwargs):
295
+ yield chunk
296
+
297
+ async def abatch(self, inputs: List[Any], config: Optional[Dict] = None, **kwargs) -> List:
298
+ """Async batch with MACAW protection."""
299
+ return self.batch(inputs, config=config, **kwargs)
300
+
301
+ def bind_to_user(self, user_client: 'MACAWClient') -> 'BoundChatAnthropic':
302
+ """
303
+ Bind this LLM to a specific user's identity.
304
+
305
+ Returns a BoundChatAnthropic that routes all calls through the user's
306
+ MACAWClient for per-user policy enforcement.
307
+
308
+ Args:
309
+ user_client: MACAWClient with user identity (from RemoteIdentityProvider)
310
+
311
+ Returns:
312
+ BoundChatAnthropic instance
313
+ """
314
+ return BoundChatAnthropic(self, user_client)
315
+
316
+ # Pass-through properties to match LangChain interface
317
+ @property
318
+ def model(self) -> str:
319
+ return self.model_name
320
+
321
+ def bind(self, **kwargs):
322
+ """Bind arguments to the LLM (LangChain compatibility)."""
323
+ new_kwargs = {**self._kwargs, **kwargs}
324
+ return ChatAnthropic(
325
+ model=self.model_name,
326
+ temperature=self.temperature,
327
+ max_tokens=self.max_tokens,
328
+ **new_kwargs
329
+ )
330
+
331
+ def with_config(self, config: Dict):
332
+ """Return self with config (LangChain compatibility)."""
333
+ return self
334
+
335
+ def cleanup(self):
336
+ """Clean up resources."""
337
+ if hasattr(self, '_secure_anthropic') and self._secure_anthropic:
338
+ try:
339
+ self._secure_anthropic.cleanup()
340
+ except Exception as e:
341
+ logger.debug(f"Cleanup error (may be expected): {e}")
342
+
343
+
344
+ class BoundChatAnthropic:
345
+ """
346
+ Per-user wrapper for ChatAnthropic.
347
+
348
+ Created via ChatAnthropic.bind_to_user(user_client).
349
+ Routes all calls through the user's identity for per-user policy enforcement.
350
+ """
351
+
352
+ def __init__(self, parent: ChatAnthropic, user_client: 'MACAWClient'):
353
+ self._parent = parent
354
+ self._user_client = user_client
355
+
356
+ # Get bound SecureAnthropic for this user
357
+ self._bound_anthropic = parent._secure_anthropic.bind_to_user(user_client)
358
+
359
+ def _to_anthropic_messages(self, input_data: Any) -> List[Dict[str, str]]:
360
+ return self._parent._to_anthropic_messages(input_data)
361
+
362
+ def _extract_system_message(self, input_data: Any) -> Optional[str]:
363
+ return self._parent._extract_system_message(input_data)
364
+
365
+ def _to_langchain_message(self, anthropic_response: Any) -> Any:
366
+ return self._parent._to_langchain_message(anthropic_response)
367
+
368
+ def _to_langchain_chunk(self, anthropic_event: Any) -> Any:
369
+ return self._parent._to_langchain_chunk(anthropic_event)
370
+
371
+ def invoke(self, input: Any, config: Optional[Dict] = None, **kwargs) -> Any:
372
+ """Invoke with user identity."""
373
+ messages = self._to_anthropic_messages(input)
374
+ system_msg = self._extract_system_message(input)
375
+
376
+ if system_msg:
377
+ messages = [m for m in messages if m.get("role") != "user" or m.get("content") != system_msg]
378
+
379
+ params = {
380
+ "model": self._parent.model_name,
381
+ "messages": messages,
382
+ "max_tokens": self._parent.max_tokens,
383
+ }
384
+ if self._parent.temperature is not None:
385
+ params["temperature"] = self._parent.temperature
386
+ if system_msg:
387
+ params["system"] = system_msg
388
+ params.update(kwargs)
389
+
390
+ # Call through bound SecureAnthropic (user identity!)
391
+ response = self._bound_anthropic.messages.create(**params)
392
+ return self._to_langchain_message(response)
393
+
394
+ def stream(self, input: Any, config: Optional[Dict] = None, **kwargs) -> Iterator:
395
+ """Stream with user identity."""
396
+ messages = self._to_anthropic_messages(input)
397
+ system_msg = self._extract_system_message(input)
398
+
399
+ if system_msg:
400
+ messages = [m for m in messages if m.get("role") != "user" or m.get("content") != system_msg]
401
+
402
+ params = {
403
+ "model": self._parent.model_name,
404
+ "messages": messages,
405
+ "max_tokens": self._parent.max_tokens,
406
+ }
407
+ if self._parent.temperature is not None:
408
+ params["temperature"] = self._parent.temperature
409
+ if system_msg:
410
+ params["system"] = system_msg
411
+ params.update(kwargs)
412
+
413
+ with self._bound_anthropic.messages.stream(**params) as stream:
414
+ for event in stream:
415
+ chunk = self._to_langchain_chunk(event)
416
+ if chunk:
417
+ yield chunk
418
+
419
+ def batch(self, inputs: List[Any], config: Optional[Dict] = None, **kwargs) -> List:
420
+ """Batch with user identity."""
421
+ return [self.invoke(input, config=config, **kwargs) for input in inputs]
422
+
423
+ async def ainvoke(self, input: Any, config: Optional[Dict] = None, **kwargs) -> Any:
424
+ """Async invoke with user identity."""
425
+ return self.invoke(input, config=config, **kwargs)
426
+
427
+ async def astream(self, input: Any, config: Optional[Dict] = None, **kwargs):
428
+ """Async stream with user identity."""
429
+ for chunk in self.stream(input, config=config, **kwargs):
430
+ yield chunk
431
+
432
+ async def abatch(self, inputs: List[Any], config: Optional[Dict] = None, **kwargs) -> List:
433
+ """Async batch with user identity."""
434
+ return self.batch(inputs, config=config, **kwargs)
435
+
436
+ @property
437
+ def model(self) -> str:
438
+ return self._parent.model_name
439
+
440
+ @property
441
+ def model_name(self) -> str:
442
+ return self._parent.model_name
443
+
444
+ @property
445
+ def temperature(self) -> float:
446
+ return self._parent.temperature
447
+
448
+
449
+ def cleanup():
450
+ """Clean up all ChatAnthropic instances."""
451
+ for instance in _instances:
452
+ try:
453
+ instance.cleanup()
454
+ except Exception as e:
455
+ logger.debug(f"Cleanup error: {e}")
456
+ _instances.clear()