@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,410 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MACAW ChatOpenAI - Drop-in replacement for langchain_openai.ChatOpenAI with MACAW protection.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
# Instead of: from langchain_openai import ChatOpenAI
|
|
6
|
+
from macaw_adapters.langchain.openai import ChatOpenAI
|
|
7
|
+
|
|
8
|
+
# Same API, MACAW security is invisible
|
|
9
|
+
llm = ChatOpenAI(model="gpt-4")
|
|
10
|
+
response = llm.invoke("Hello, world!")
|
|
11
|
+
|
|
12
|
+
Security:
|
|
13
|
+
This adapter uses SecureOpenAI 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 ChatOpenAI instances for cleanup
|
|
26
|
+
_instances: List['ChatOpenAI'] = []
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ChatOpenAI:
|
|
30
|
+
"""
|
|
31
|
+
Drop-in replacement for langchain_openai.ChatOpenAI with MACAW protection.
|
|
32
|
+
|
|
33
|
+
Uses SecureOpenAI 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 = "gpt-4",
|
|
42
|
+
temperature: float = 0.7,
|
|
43
|
+
max_tokens: Optional[int] = None,
|
|
44
|
+
api_key: Optional[str] = None,
|
|
45
|
+
base_url: Optional[str] = None,
|
|
46
|
+
organization: Optional[str] = None,
|
|
47
|
+
timeout: Optional[float] = None,
|
|
48
|
+
max_retries: int = 2,
|
|
49
|
+
**kwargs
|
|
50
|
+
):
|
|
51
|
+
"""
|
|
52
|
+
Initialize SecureChatOpenAI.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
model: OpenAI model name (e.g., "gpt-4", "gpt-3.5-turbo")
|
|
56
|
+
temperature: Sampling temperature (0.0 to 2.0)
|
|
57
|
+
max_tokens: Maximum tokens to generate
|
|
58
|
+
api_key: OpenAI API key (or use OPENAI_API_KEY env var)
|
|
59
|
+
base_url: Custom API base URL
|
|
60
|
+
organization: OpenAI organization ID
|
|
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 SecureOpenAI)
|
|
72
|
+
self._base_url = base_url
|
|
73
|
+
self._organization = organization
|
|
74
|
+
self._timeout = timeout
|
|
75
|
+
self._max_retries = max_retries
|
|
76
|
+
|
|
77
|
+
# Create SecureOpenAI internally - it handles its own MACAWClient
|
|
78
|
+
# SecureOpenAI accepts: api_key, app_name, intent_policy
|
|
79
|
+
from macaw_adapters.openai import SecureOpenAI
|
|
80
|
+
|
|
81
|
+
self._secure_openai = SecureOpenAI(
|
|
82
|
+
app_name="langchain-openai",
|
|
83
|
+
api_key=api_key
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# Track for cleanup
|
|
87
|
+
_instances.append(self)
|
|
88
|
+
|
|
89
|
+
logger.debug(f"[ChatOpenAI] Created with SecureOpenAI backend, model={model}")
|
|
90
|
+
|
|
91
|
+
def _to_openai_messages(self, input_data: Any) -> List[Dict[str, str]]:
|
|
92
|
+
"""Convert LangChain input to OpenAI message format."""
|
|
93
|
+
if isinstance(input_data, str):
|
|
94
|
+
return [{"role": "user", "content": input_data}]
|
|
95
|
+
|
|
96
|
+
if isinstance(input_data, list):
|
|
97
|
+
messages = []
|
|
98
|
+
for msg in input_data:
|
|
99
|
+
if hasattr(msg, 'type') and hasattr(msg, 'content'):
|
|
100
|
+
# LangChain message object (HumanMessage, AIMessage, etc.)
|
|
101
|
+
role_map = {
|
|
102
|
+
"human": "user",
|
|
103
|
+
"ai": "assistant",
|
|
104
|
+
"system": "system",
|
|
105
|
+
"function": "function",
|
|
106
|
+
"tool": "tool"
|
|
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 _to_langchain_message(self, openai_response: Any) -> Any:
|
|
122
|
+
"""Convert OpenAI ChatCompletion to LangChain AIMessage."""
|
|
123
|
+
try:
|
|
124
|
+
from langchain_core.messages import AIMessage
|
|
125
|
+
|
|
126
|
+
# Handle dict response
|
|
127
|
+
if isinstance(openai_response, dict):
|
|
128
|
+
choices = openai_response.get("choices", [])
|
|
129
|
+
if choices:
|
|
130
|
+
content = choices[0].get("message", {}).get("content", "")
|
|
131
|
+
return AIMessage(content=content)
|
|
132
|
+
return AIMessage(content="")
|
|
133
|
+
|
|
134
|
+
# Handle ChatCompletion object
|
|
135
|
+
if hasattr(openai_response, 'choices') and openai_response.choices:
|
|
136
|
+
content = openai_response.choices[0].message.content or ""
|
|
137
|
+
return AIMessage(content=content)
|
|
138
|
+
|
|
139
|
+
return AIMessage(content=str(openai_response))
|
|
140
|
+
|
|
141
|
+
except ImportError:
|
|
142
|
+
# langchain_core not available, return raw response
|
|
143
|
+
logger.warning("langchain_core not installed, returning raw response")
|
|
144
|
+
return openai_response
|
|
145
|
+
|
|
146
|
+
def _to_langchain_chunk(self, openai_chunk: Any) -> Any:
|
|
147
|
+
"""Convert OpenAI streaming chunk to LangChain AIMessageChunk."""
|
|
148
|
+
try:
|
|
149
|
+
from langchain_core.messages import AIMessageChunk
|
|
150
|
+
|
|
151
|
+
# Handle dict chunk
|
|
152
|
+
if isinstance(openai_chunk, dict):
|
|
153
|
+
choices = openai_chunk.get("choices", [])
|
|
154
|
+
if choices:
|
|
155
|
+
delta = choices[0].get("delta", {})
|
|
156
|
+
content = delta.get("content", "")
|
|
157
|
+
return AIMessageChunk(content=content)
|
|
158
|
+
return AIMessageChunk(content="")
|
|
159
|
+
|
|
160
|
+
# Handle ChatCompletionChunk object
|
|
161
|
+
if hasattr(openai_chunk, 'choices') and openai_chunk.choices:
|
|
162
|
+
delta = openai_chunk.choices[0].delta
|
|
163
|
+
content = delta.content if delta and delta.content else ""
|
|
164
|
+
return AIMessageChunk(content=content)
|
|
165
|
+
|
|
166
|
+
return AIMessageChunk(content="")
|
|
167
|
+
|
|
168
|
+
except ImportError:
|
|
169
|
+
# Return raw chunk
|
|
170
|
+
return openai_chunk
|
|
171
|
+
|
|
172
|
+
def invoke(self, input: Any, config: Optional[Dict] = None, **kwargs) -> Any:
|
|
173
|
+
"""
|
|
174
|
+
Invoke the LLM with MACAW protection.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
input: The input prompt (string or list of messages)
|
|
178
|
+
config: Optional configuration dict
|
|
179
|
+
**kwargs: Additional arguments
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
LLM response (AIMessage)
|
|
183
|
+
"""
|
|
184
|
+
# Convert to OpenAI format
|
|
185
|
+
messages = self._to_openai_messages(input)
|
|
186
|
+
|
|
187
|
+
# Build params
|
|
188
|
+
params = {
|
|
189
|
+
"model": self.model_name,
|
|
190
|
+
"messages": messages,
|
|
191
|
+
"temperature": self.temperature,
|
|
192
|
+
}
|
|
193
|
+
if self.max_tokens:
|
|
194
|
+
params["max_tokens"] = self.max_tokens
|
|
195
|
+
|
|
196
|
+
# Merge additional kwargs
|
|
197
|
+
params.update(kwargs)
|
|
198
|
+
|
|
199
|
+
# Call SecureOpenAI (routes through PEP!)
|
|
200
|
+
response = self._secure_openai.chat.completions.create(**params)
|
|
201
|
+
|
|
202
|
+
# Convert to LangChain format
|
|
203
|
+
return self._to_langchain_message(response)
|
|
204
|
+
|
|
205
|
+
def stream(self, input: Any, config: Optional[Dict] = None, **kwargs) -> Iterator:
|
|
206
|
+
"""
|
|
207
|
+
Stream responses from the LLM with MACAW protection.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
input: The input prompt
|
|
211
|
+
config: Optional configuration dict
|
|
212
|
+
**kwargs: Additional arguments
|
|
213
|
+
|
|
214
|
+
Yields:
|
|
215
|
+
Streaming chunks (AIMessageChunk)
|
|
216
|
+
"""
|
|
217
|
+
# Convert to OpenAI format
|
|
218
|
+
messages = self._to_openai_messages(input)
|
|
219
|
+
|
|
220
|
+
# Build params
|
|
221
|
+
params = {
|
|
222
|
+
"model": self.model_name,
|
|
223
|
+
"messages": messages,
|
|
224
|
+
"temperature": self.temperature,
|
|
225
|
+
"stream": True,
|
|
226
|
+
}
|
|
227
|
+
if self.max_tokens:
|
|
228
|
+
params["max_tokens"] = self.max_tokens
|
|
229
|
+
|
|
230
|
+
params.update(kwargs)
|
|
231
|
+
|
|
232
|
+
# Stream from SecureOpenAI (routes through PEP!)
|
|
233
|
+
for chunk in self._secure_openai.chat.completions.create(**params):
|
|
234
|
+
yield self._to_langchain_chunk(chunk)
|
|
235
|
+
|
|
236
|
+
def batch(self, inputs: List[Any], config: Optional[Dict] = None, **kwargs) -> List:
|
|
237
|
+
"""
|
|
238
|
+
Batch invoke the LLM with MACAW protection.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
inputs: List of input prompts
|
|
242
|
+
config: Optional configuration dict
|
|
243
|
+
**kwargs: Additional arguments
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
List of LLM responses (AIMessage)
|
|
247
|
+
"""
|
|
248
|
+
# Process each input through invoke (each goes through PEP)
|
|
249
|
+
return [self.invoke(input, config=config, **kwargs) for input in inputs]
|
|
250
|
+
|
|
251
|
+
async def ainvoke(self, input: Any, config: Optional[Dict] = None, **kwargs) -> Any:
|
|
252
|
+
"""Async invoke with MACAW protection."""
|
|
253
|
+
# For now, delegate to sync version
|
|
254
|
+
# TODO: Add true async support when SecureOpenAI supports it
|
|
255
|
+
return self.invoke(input, config=config, **kwargs)
|
|
256
|
+
|
|
257
|
+
async def astream(self, input: Any, config: Optional[Dict] = None, **kwargs):
|
|
258
|
+
"""Async stream with MACAW protection."""
|
|
259
|
+
# Yield from sync stream
|
|
260
|
+
for chunk in self.stream(input, config=config, **kwargs):
|
|
261
|
+
yield chunk
|
|
262
|
+
|
|
263
|
+
async def abatch(self, inputs: List[Any], config: Optional[Dict] = None, **kwargs) -> List:
|
|
264
|
+
"""Async batch with MACAW protection."""
|
|
265
|
+
return self.batch(inputs, config=config, **kwargs)
|
|
266
|
+
|
|
267
|
+
def bind_to_user(self, user_client: 'MACAWClient') -> 'BoundChatOpenAI':
|
|
268
|
+
"""
|
|
269
|
+
Bind this LLM to a specific user's identity.
|
|
270
|
+
|
|
271
|
+
Returns a BoundChatOpenAI that routes all calls through the user's
|
|
272
|
+
MACAWClient for per-user policy enforcement.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
user_client: MACAWClient with user identity (from RemoteIdentityProvider)
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
BoundChatOpenAI instance
|
|
279
|
+
"""
|
|
280
|
+
return BoundChatOpenAI(self, user_client)
|
|
281
|
+
|
|
282
|
+
# Pass-through properties to match LangChain interface
|
|
283
|
+
@property
|
|
284
|
+
def model(self) -> str:
|
|
285
|
+
return self.model_name
|
|
286
|
+
|
|
287
|
+
def bind(self, **kwargs):
|
|
288
|
+
"""Bind arguments to the LLM (LangChain compatibility)."""
|
|
289
|
+
# Create a new instance with merged kwargs
|
|
290
|
+
new_kwargs = {**self._kwargs, **kwargs}
|
|
291
|
+
return ChatOpenAI(
|
|
292
|
+
model=self.model_name,
|
|
293
|
+
temperature=self.temperature,
|
|
294
|
+
max_tokens=self.max_tokens,
|
|
295
|
+
**new_kwargs
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
def with_config(self, config: Dict):
|
|
299
|
+
"""Return self with config (LangChain compatibility)."""
|
|
300
|
+
# Config is handled per-call, not stored
|
|
301
|
+
return self
|
|
302
|
+
|
|
303
|
+
def cleanup(self):
|
|
304
|
+
"""Clean up resources."""
|
|
305
|
+
if hasattr(self, '_secure_openai') and self._secure_openai:
|
|
306
|
+
try:
|
|
307
|
+
self._secure_openai.cleanup()
|
|
308
|
+
except Exception as e:
|
|
309
|
+
logger.debug(f"Cleanup error (may be expected): {e}")
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
class BoundChatOpenAI:
|
|
313
|
+
"""
|
|
314
|
+
Per-user wrapper for ChatOpenAI.
|
|
315
|
+
|
|
316
|
+
Created via ChatOpenAI.bind_to_user(user_client).
|
|
317
|
+
Routes all calls through the user's identity for per-user policy enforcement.
|
|
318
|
+
"""
|
|
319
|
+
|
|
320
|
+
def __init__(self, parent: ChatOpenAI, user_client: 'MACAWClient'):
|
|
321
|
+
self._parent = parent
|
|
322
|
+
self._user_client = user_client
|
|
323
|
+
|
|
324
|
+
# Get bound SecureOpenAI for this user
|
|
325
|
+
self._bound_openai = parent._secure_openai.bind_to_user(user_client)
|
|
326
|
+
|
|
327
|
+
def _to_openai_messages(self, input_data: Any) -> List[Dict[str, str]]:
|
|
328
|
+
"""Delegate to parent's conversion."""
|
|
329
|
+
return self._parent._to_openai_messages(input_data)
|
|
330
|
+
|
|
331
|
+
def _to_langchain_message(self, openai_response: Any) -> Any:
|
|
332
|
+
"""Delegate to parent's conversion."""
|
|
333
|
+
return self._parent._to_langchain_message(openai_response)
|
|
334
|
+
|
|
335
|
+
def _to_langchain_chunk(self, openai_chunk: Any) -> Any:
|
|
336
|
+
"""Delegate to parent's conversion."""
|
|
337
|
+
return self._parent._to_langchain_chunk(openai_chunk)
|
|
338
|
+
|
|
339
|
+
def invoke(self, input: Any, config: Optional[Dict] = None, **kwargs) -> Any:
|
|
340
|
+
"""Invoke with user identity."""
|
|
341
|
+
messages = self._to_openai_messages(input)
|
|
342
|
+
|
|
343
|
+
params = {
|
|
344
|
+
"model": self._parent.model_name,
|
|
345
|
+
"messages": messages,
|
|
346
|
+
"temperature": self._parent.temperature,
|
|
347
|
+
}
|
|
348
|
+
if self._parent.max_tokens:
|
|
349
|
+
params["max_tokens"] = self._parent.max_tokens
|
|
350
|
+
params.update(kwargs)
|
|
351
|
+
|
|
352
|
+
# Call through bound SecureOpenAI (user identity!)
|
|
353
|
+
response = self._bound_openai.chat.completions.create(**params)
|
|
354
|
+
return self._to_langchain_message(response)
|
|
355
|
+
|
|
356
|
+
def stream(self, input: Any, config: Optional[Dict] = None, **kwargs) -> Iterator:
|
|
357
|
+
"""Stream with user identity."""
|
|
358
|
+
messages = self._to_openai_messages(input)
|
|
359
|
+
|
|
360
|
+
params = {
|
|
361
|
+
"model": self._parent.model_name,
|
|
362
|
+
"messages": messages,
|
|
363
|
+
"temperature": self._parent.temperature,
|
|
364
|
+
"stream": True,
|
|
365
|
+
}
|
|
366
|
+
if self._parent.max_tokens:
|
|
367
|
+
params["max_tokens"] = self._parent.max_tokens
|
|
368
|
+
params.update(kwargs)
|
|
369
|
+
|
|
370
|
+
for chunk in self._bound_openai.chat.completions.create(**params):
|
|
371
|
+
yield self._to_langchain_chunk(chunk)
|
|
372
|
+
|
|
373
|
+
def batch(self, inputs: List[Any], config: Optional[Dict] = None, **kwargs) -> List:
|
|
374
|
+
"""Batch with user identity."""
|
|
375
|
+
return [self.invoke(input, config=config, **kwargs) for input in inputs]
|
|
376
|
+
|
|
377
|
+
async def ainvoke(self, input: Any, config: Optional[Dict] = None, **kwargs) -> Any:
|
|
378
|
+
"""Async invoke with user identity."""
|
|
379
|
+
return self.invoke(input, config=config, **kwargs)
|
|
380
|
+
|
|
381
|
+
async def astream(self, input: Any, config: Optional[Dict] = None, **kwargs):
|
|
382
|
+
"""Async stream with user identity."""
|
|
383
|
+
for chunk in self.stream(input, config=config, **kwargs):
|
|
384
|
+
yield chunk
|
|
385
|
+
|
|
386
|
+
async def abatch(self, inputs: List[Any], config: Optional[Dict] = None, **kwargs) -> List:
|
|
387
|
+
"""Async batch with user identity."""
|
|
388
|
+
return self.batch(inputs, config=config, **kwargs)
|
|
389
|
+
|
|
390
|
+
@property
|
|
391
|
+
def model(self) -> str:
|
|
392
|
+
return self._parent.model_name
|
|
393
|
+
|
|
394
|
+
@property
|
|
395
|
+
def model_name(self) -> str:
|
|
396
|
+
return self._parent.model_name
|
|
397
|
+
|
|
398
|
+
@property
|
|
399
|
+
def temperature(self) -> float:
|
|
400
|
+
return self._parent.temperature
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def cleanup():
|
|
404
|
+
"""Clean up all ChatOpenAI instances."""
|
|
405
|
+
for instance in _instances:
|
|
406
|
+
try:
|
|
407
|
+
instance.cleanup()
|
|
408
|
+
except Exception as e:
|
|
409
|
+
logger.debug(f"Cleanup error: {e}")
|
|
410
|
+
_instances.clear()
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MACAW Tool Wrappers - LangChain tools routed through MACAW PEP.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
from macaw_adapters.langchain.tools import SecureToolWrapper, wrap_tools
|
|
6
|
+
|
|
7
|
+
# Wrap individual tool
|
|
8
|
+
secure_tool = SecureToolWrapper(original_tool, macaw_client)
|
|
9
|
+
|
|
10
|
+
# Wrap list of tools
|
|
11
|
+
secure_tools = wrap_tools(tools, macaw_client)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
from typing import Any, List, Optional
|
|
16
|
+
|
|
17
|
+
from langchain_core.tools import BaseTool
|
|
18
|
+
from langchain_core.callbacks import CallbackManagerForToolRun, AsyncCallbackManagerForToolRun
|
|
19
|
+
from macaw_client import MACAWClient
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SecureToolWrapper(BaseTool):
|
|
25
|
+
"""
|
|
26
|
+
Wrapper for LangChain tools that routes execution through MACAW PEP.
|
|
27
|
+
|
|
28
|
+
Inherits from BaseTool to be fully compatible with LangChain's
|
|
29
|
+
AgentExecutor and validation. Enforces security policies via MACAWClient.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
# Pydantic fields
|
|
33
|
+
original_tool: Any = None
|
|
34
|
+
macaw_client: Any = None
|
|
35
|
+
|
|
36
|
+
class Config:
|
|
37
|
+
arbitrary_types_allowed = True
|
|
38
|
+
|
|
39
|
+
def __init__(self, original_tool: Any, macaw_client: MACAWClient, **kwargs):
|
|
40
|
+
"""
|
|
41
|
+
Initialize secure tool wrapper.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
original_tool: LangChain BaseTool instance
|
|
45
|
+
macaw_client: MACAWClient for policy enforcement
|
|
46
|
+
"""
|
|
47
|
+
# Extract attributes from original tool for BaseTool
|
|
48
|
+
super().__init__(
|
|
49
|
+
name=original_tool.name,
|
|
50
|
+
description=original_tool.description,
|
|
51
|
+
original_tool=original_tool,
|
|
52
|
+
macaw_client=macaw_client,
|
|
53
|
+
**kwargs
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Copy optional attributes
|
|
57
|
+
if hasattr(original_tool, 'args_schema') and original_tool.args_schema:
|
|
58
|
+
self.args_schema = original_tool.args_schema
|
|
59
|
+
if hasattr(original_tool, 'return_direct'):
|
|
60
|
+
self.return_direct = original_tool.return_direct
|
|
61
|
+
|
|
62
|
+
# Register tool with MAPL-compliant name: tool:<app_name>/<tool_name>
|
|
63
|
+
app_name = getattr(macaw_client, 'app_name', 'langchain')
|
|
64
|
+
mapl_name = f"tool:{app_name}/{original_tool.name}"
|
|
65
|
+
|
|
66
|
+
def tool_handler(params):
|
|
67
|
+
"""Handler that executes the original tool."""
|
|
68
|
+
tool_input = params.get("input", "")
|
|
69
|
+
return original_tool.run(tool_input)
|
|
70
|
+
|
|
71
|
+
macaw_client.register_tool(mapl_name, tool_handler)
|
|
72
|
+
logger.info(f"Registered secure tool: {original_tool.name} -> {mapl_name}")
|
|
73
|
+
|
|
74
|
+
def _run(
|
|
75
|
+
self,
|
|
76
|
+
tool_input: str = "",
|
|
77
|
+
run_manager: Optional[CallbackManagerForToolRun] = None,
|
|
78
|
+
**kwargs
|
|
79
|
+
) -> str:
|
|
80
|
+
"""Execute tool through MACAW PEP."""
|
|
81
|
+
# Use MAPL-compliant name: tool:<app_name>/<tool_name>
|
|
82
|
+
app_name = getattr(self.macaw_client, 'app_name', 'langchain')
|
|
83
|
+
mapl_name = f"tool:{app_name}/{self.name}"
|
|
84
|
+
logger.debug(f"[SecureTool] Routing {self.name} -> {mapl_name} through MACAW")
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
# Build parameters dict
|
|
88
|
+
parameters = {"input": tool_input}
|
|
89
|
+
|
|
90
|
+
# Invoke tool through MACAW protocol with MAPL-compliant name
|
|
91
|
+
# Authenticated prompts are auto-created by invoke_tool() based on registry
|
|
92
|
+
result = self.macaw_client.invoke_tool(
|
|
93
|
+
tool_name=mapl_name,
|
|
94
|
+
parameters=parameters,
|
|
95
|
+
target_agent=self.macaw_client.agent_id
|
|
96
|
+
)
|
|
97
|
+
return str(result) if result is not None else ""
|
|
98
|
+
|
|
99
|
+
except Exception as e:
|
|
100
|
+
error_msg = str(e).lower()
|
|
101
|
+
if any(word in error_msg for word in ['denied', 'blocked', 'policy']):
|
|
102
|
+
logger.warning(f"MACAW blocked {mapl_name}: {e}")
|
|
103
|
+
return f"Access denied by security policy: {mapl_name}"
|
|
104
|
+
raise
|
|
105
|
+
|
|
106
|
+
async def _arun(
|
|
107
|
+
self,
|
|
108
|
+
tool_input: str = "",
|
|
109
|
+
run_manager: Optional[AsyncCallbackManagerForToolRun] = None,
|
|
110
|
+
**kwargs
|
|
111
|
+
) -> str:
|
|
112
|
+
"""Async execution (delegates to sync)."""
|
|
113
|
+
return self._run(tool_input, **kwargs)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def wrap_tools(tools: List[Any], macaw_client: MACAWClient) -> List[SecureToolWrapper]:
|
|
117
|
+
"""
|
|
118
|
+
Wrap a list of LangChain tools with MACAW security.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
tools: List of LangChain BaseTool instances
|
|
122
|
+
macaw_client: MACAWClient for policy enforcement
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
List of SecureToolWrapper instances
|
|
126
|
+
"""
|
|
127
|
+
return [SecureToolWrapper(tool, macaw_client) for tool in tools]
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def secure_tool(func=None, *, macaw_client: Optional[MACAWClient] = None):
|
|
131
|
+
"""
|
|
132
|
+
Decorator to create a secure LangChain tool with MACAW protection.
|
|
133
|
+
|
|
134
|
+
Usage:
|
|
135
|
+
@secure_tool
|
|
136
|
+
def search_database(query: str) -> str:
|
|
137
|
+
'''Search the database for relevant information.'''
|
|
138
|
+
return database.search(query)
|
|
139
|
+
|
|
140
|
+
# Or with explicit client:
|
|
141
|
+
@secure_tool(macaw_client=my_client)
|
|
142
|
+
def my_tool(x: str) -> str:
|
|
143
|
+
'''My tool description.'''
|
|
144
|
+
return process(x)
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
func: The function to wrap (used when called without parentheses)
|
|
148
|
+
macaw_client: Optional MACAWClient for policy enforcement
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
A SecureToolWrapper compatible with LangChain agents
|
|
152
|
+
"""
|
|
153
|
+
from langchain_core.tools import tool as langchain_tool
|
|
154
|
+
from ._utils import get_or_create_client
|
|
155
|
+
|
|
156
|
+
def decorator(fn):
|
|
157
|
+
# Create LangChain tool from function
|
|
158
|
+
lc_tool = langchain_tool(fn)
|
|
159
|
+
|
|
160
|
+
# Get or create MACAW client
|
|
161
|
+
client = macaw_client or get_or_create_client("langchain-tools")
|
|
162
|
+
if client is None:
|
|
163
|
+
logger.warning("Could not create MACAWClient, returning unwrapped tool")
|
|
164
|
+
return lc_tool
|
|
165
|
+
|
|
166
|
+
# Wrap with MACAW security
|
|
167
|
+
return SecureToolWrapper(lc_tool, client)
|
|
168
|
+
|
|
169
|
+
if func is not None:
|
|
170
|
+
# Called without parentheses: @secure_tool
|
|
171
|
+
return decorator(func)
|
|
172
|
+
else:
|
|
173
|
+
# Called with parentheses: @secure_tool() or @secure_tool(macaw_client=...)
|
|
174
|
+
return decorator
|