@macaw-ai/client 0.0.0-stage → 0.9.11
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 +422 -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,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()
|