@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,262 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
langchain_1b_multiuser.py - Multi-user Agents with role-based policies
|
|
4
|
+
|
|
5
|
+
Create different agent instances per user role, each with their own security policy.
|
|
6
|
+
Use this when: Different users need different tool access levels.
|
|
7
|
+
|
|
8
|
+
In this example:
|
|
9
|
+
- alice (Analyst): Can only use calculator
|
|
10
|
+
- bob (Manager): Can use calculator and weather
|
|
11
|
+
- carol (Admin): Can use all tools including admin
|
|
12
|
+
|
|
13
|
+
Prerequisites:
|
|
14
|
+
- MACAW SDK installed (pip install macaw-client macaw-adapters)
|
|
15
|
+
- OPENAI_API_KEY environment variable (for ChatOpenAI)
|
|
16
|
+
|
|
17
|
+
Run:
|
|
18
|
+
export OPENAI_API_KEY=sk-...
|
|
19
|
+
python langchain_1b_multiuser.py
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import os
|
|
23
|
+
|
|
24
|
+
from macaw_adapters.langchain import create_react_agent, AgentExecutor, cleanup
|
|
25
|
+
|
|
26
|
+
from langchain_openai import ChatOpenAI
|
|
27
|
+
from langchain.tools import Tool
|
|
28
|
+
from langchain.prompts import PromptTemplate
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# Define tools
|
|
32
|
+
def calculator(expression: str) -> str:
|
|
33
|
+
"""Evaluate a math expression."""
|
|
34
|
+
try:
|
|
35
|
+
result = eval(expression)
|
|
36
|
+
return f"Result: {result}"
|
|
37
|
+
except Exception as e:
|
|
38
|
+
return f"Error: {e}"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_weather(city: str) -> str:
|
|
42
|
+
"""Get weather for a city (mock)."""
|
|
43
|
+
weather_data = {
|
|
44
|
+
"new york": "72°F, Sunny",
|
|
45
|
+
"london": "58°F, Cloudy",
|
|
46
|
+
"tokyo": "68°F, Clear"
|
|
47
|
+
}
|
|
48
|
+
return weather_data.get(city.lower(), f"Weather for {city}: 65°F, Partly cloudy")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def admin_tool(command: str) -> str:
|
|
52
|
+
"""Execute admin command."""
|
|
53
|
+
return f"Executed admin command: {command}"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def file_reader(path: str) -> str:
|
|
57
|
+
"""Read a file."""
|
|
58
|
+
# Mock - returns different content based on path
|
|
59
|
+
if "report" in path.lower():
|
|
60
|
+
return "Q4 Revenue: $1.2M, Growth: 15%"
|
|
61
|
+
elif "secret" in path.lower():
|
|
62
|
+
return "ACCESS DENIED - Classified content"
|
|
63
|
+
return f"Contents of {path}: [mock data]"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# All available tools
|
|
67
|
+
ALL_TOOLS = [
|
|
68
|
+
Tool(name="calculator", func=calculator, description="Math calculations"),
|
|
69
|
+
Tool(name="weather", func=get_weather, description="Get weather for a city"),
|
|
70
|
+
Tool(name="admin", func=admin_tool, description="Execute admin commands"),
|
|
71
|
+
Tool(name="file_reader", func=file_reader, description="Read files"),
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# User role configurations with MAPL format policies
|
|
76
|
+
USER_CONFIGS = {
|
|
77
|
+
"alice": {
|
|
78
|
+
"role": "Financial Analyst",
|
|
79
|
+
"security_policy": {
|
|
80
|
+
"resources": ["tool:calculator", "tool:file_reader"],
|
|
81
|
+
"denied_resources": ["tool:admin"],
|
|
82
|
+
"constraints": {
|
|
83
|
+
"parameters": {
|
|
84
|
+
"tool:file_reader": {"input": ["*report*", "*public*"]}
|
|
85
|
+
},
|
|
86
|
+
"denied_parameters": {
|
|
87
|
+
"tool:*": {"input": ["*password*", "*secret*"]}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
"tests": [
|
|
92
|
+
("What is 100 * 12?", "ALLOWED - calculator"),
|
|
93
|
+
("Read the Q4 report", "ALLOWED - file_reader for reports"),
|
|
94
|
+
("Run admin command: list users", "BLOCKED - admin tool"),
|
|
95
|
+
("What's the weather in London?", "BLOCKED - weather not allowed"),
|
|
96
|
+
]
|
|
97
|
+
},
|
|
98
|
+
"bob": {
|
|
99
|
+
"role": "Finance Manager",
|
|
100
|
+
"security_policy": {
|
|
101
|
+
"resources": ["tool:calculator", "tool:weather", "tool:file_reader"],
|
|
102
|
+
"denied_resources": ["tool:admin"],
|
|
103
|
+
"constraints": {
|
|
104
|
+
"parameters": {
|
|
105
|
+
"tool:file_reader": {"input": ["*report*", "*forecast*", "*public*"]}
|
|
106
|
+
},
|
|
107
|
+
"denied_parameters": {
|
|
108
|
+
"tool:*": {"input": ["*password*"]}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
"tests": [
|
|
113
|
+
("What is 250 + 750?", "ALLOWED - calculator"),
|
|
114
|
+
("What's the weather in Tokyo?", "ALLOWED - weather"),
|
|
115
|
+
("Read the Q4 report", "ALLOWED - file_reader"),
|
|
116
|
+
("Run admin command: restart", "BLOCKED - admin tool"),
|
|
117
|
+
]
|
|
118
|
+
},
|
|
119
|
+
"carol": {
|
|
120
|
+
"role": "IT Administrator",
|
|
121
|
+
"security_policy": {
|
|
122
|
+
"resources": ["tool:calculator", "tool:weather", "tool:file_reader", "tool:admin"],
|
|
123
|
+
# No denied_resources - full tool access
|
|
124
|
+
"constraints": {
|
|
125
|
+
"parameters": {
|
|
126
|
+
"tool:file_reader": {"input": ["*"]} # All files
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
"tests": [
|
|
131
|
+
("What is 999 * 111?", "ALLOWED - calculator"),
|
|
132
|
+
("What's the weather in New York?", "ALLOWED - weather"),
|
|
133
|
+
("Run admin command: status check", "ALLOWED - admin access"),
|
|
134
|
+
("Read the secret file", "ALLOWED - full file access"),
|
|
135
|
+
]
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# Simple ReAct prompt
|
|
141
|
+
REACT_PROMPT = PromptTemplate.from_template("""Answer the following questions as best you can. You have access to the following tools:
|
|
142
|
+
|
|
143
|
+
{tools}
|
|
144
|
+
|
|
145
|
+
Use the following format:
|
|
146
|
+
|
|
147
|
+
Question: the input question you must answer
|
|
148
|
+
Thought: you should always think about what to do
|
|
149
|
+
Action: the action to take, should be one of [{tool_names}]
|
|
150
|
+
Action Input: the input to the action
|
|
151
|
+
Observation: the result of the action
|
|
152
|
+
... (this Thought/Action/Action Input/Observation can repeat N times)
|
|
153
|
+
Thought: I now know the final answer
|
|
154
|
+
Final Answer: the final answer to the original input question
|
|
155
|
+
|
|
156
|
+
Begin!
|
|
157
|
+
|
|
158
|
+
Question: {input}
|
|
159
|
+
Thought:{agent_scratchpad}""")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def create_user_executor(username: str, config: dict, llm) -> AgentExecutor:
|
|
163
|
+
"""Create a security-scoped executor for a specific user."""
|
|
164
|
+
print(f"\n Creating executor for {username} ({config['role']})")
|
|
165
|
+
print(f" Policy: {config['security_policy'].get('resources', 'all')}")
|
|
166
|
+
|
|
167
|
+
agent = create_react_agent(
|
|
168
|
+
llm=llm,
|
|
169
|
+
tools=ALL_TOOLS,
|
|
170
|
+
prompt=REACT_PROMPT,
|
|
171
|
+
security_policy=config["security_policy"]
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
executor = AgentExecutor(
|
|
175
|
+
agent=agent,
|
|
176
|
+
tools=ALL_TOOLS,
|
|
177
|
+
security_policy=config["security_policy"],
|
|
178
|
+
verbose=False,
|
|
179
|
+
handle_parsing_errors=True,
|
|
180
|
+
max_iterations=3
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
return executor
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def test_user(username: str, config: dict, executor: AgentExecutor):
|
|
187
|
+
"""Test a user's executor with their test cases."""
|
|
188
|
+
print(f"\n{'=' * 60}")
|
|
189
|
+
print(f"Testing {username.upper()} ({config['role']})")
|
|
190
|
+
print(f"Allowed tools: {config['security_policy'].get('resources', 'all')}")
|
|
191
|
+
print("=" * 60)
|
|
192
|
+
|
|
193
|
+
for query, expected in config["tests"]:
|
|
194
|
+
print(f"\n Query: {query}")
|
|
195
|
+
print(f" Expected: {expected}")
|
|
196
|
+
|
|
197
|
+
try:
|
|
198
|
+
result = executor.invoke({"input": query})
|
|
199
|
+
output = result.get("output", str(result))
|
|
200
|
+
|
|
201
|
+
# Check if it was blocked
|
|
202
|
+
if "denied" in output.lower() or "access denied" in output.lower():
|
|
203
|
+
print(f" Result: BLOCKED - {output[:50]}...")
|
|
204
|
+
elif "cannot" in output.lower() or "don't have" in output.lower():
|
|
205
|
+
print(f" Result: BLOCKED (agent refused) - {output[:50]}...")
|
|
206
|
+
else:
|
|
207
|
+
print(f" Result: SUCCESS - {output[:60]}...")
|
|
208
|
+
|
|
209
|
+
except Exception as e:
|
|
210
|
+
error_msg = str(e).lower()
|
|
211
|
+
if any(word in error_msg for word in ["denied", "blocked", "policy", "not allowed"]):
|
|
212
|
+
print(f" Result: CORRECTLY BLOCKED - {str(e)[:50]}...")
|
|
213
|
+
else:
|
|
214
|
+
print(f" Result: ERROR - {str(e)[:50]}...")
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def main():
|
|
218
|
+
if not os.environ.get("OPENAI_API_KEY"):
|
|
219
|
+
print("Set OPENAI_API_KEY environment variable")
|
|
220
|
+
return
|
|
221
|
+
|
|
222
|
+
print("=" * 60)
|
|
223
|
+
print("Example 1b: Multi-user Agents (LangChain)")
|
|
224
|
+
print("=" * 60)
|
|
225
|
+
print("\nPattern: Separate executor per user with role-based policies")
|
|
226
|
+
print("Use when: Different users need different tool access levels")
|
|
227
|
+
print("\nUsers:")
|
|
228
|
+
print(" alice (Analyst): calculator, file_reader (reports only)")
|
|
229
|
+
print(" bob (Manager): calculator, weather, file_reader")
|
|
230
|
+
print(" carol (Admin): all tools, all files")
|
|
231
|
+
|
|
232
|
+
# Shared LLM
|
|
233
|
+
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
|
|
234
|
+
|
|
235
|
+
# Create and test each user
|
|
236
|
+
for username, config in USER_CONFIGS.items():
|
|
237
|
+
executor = create_user_executor(username, config, llm)
|
|
238
|
+
test_user(username, config, executor)
|
|
239
|
+
|
|
240
|
+
# Cleanup
|
|
241
|
+
print("\n--- Cleanup ---")
|
|
242
|
+
cleanup()
|
|
243
|
+
|
|
244
|
+
print("\n" + "=" * 60)
|
|
245
|
+
print("Example complete!")
|
|
246
|
+
print("=" * 60)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
if __name__ == "__main__":
|
|
250
|
+
try:
|
|
251
|
+
main()
|
|
252
|
+
except Exception as e:
|
|
253
|
+
err = str(e)
|
|
254
|
+
print("\n" + "=" * 60)
|
|
255
|
+
if "OPENAI_API_KEY" in err or "api_key" in err.lower():
|
|
256
|
+
print("ERROR: OpenAI API key not configured")
|
|
257
|
+
print("Fix: export OPENAI_API_KEY=sk-...")
|
|
258
|
+
else:
|
|
259
|
+
print(f"ERROR: {e}")
|
|
260
|
+
print("=" * 60)
|
|
261
|
+
import sys
|
|
262
|
+
sys.exit(1)
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
langchain_1c_orchestration.py - Agent Orchestration with supervisor routing
|
|
4
|
+
|
|
5
|
+
A supervisor agent routes tasks to specialized sub-agents, each with different
|
|
6
|
+
security policies. Demonstrates policy composition where chains can only
|
|
7
|
+
restrict, never relax permissions.
|
|
8
|
+
|
|
9
|
+
Use this when: Building multi-agent workflows with different permission levels.
|
|
10
|
+
|
|
11
|
+
In this example:
|
|
12
|
+
- Supervisor: Routes requests to appropriate agent
|
|
13
|
+
- Research Agent: Can use search, file_reader (public only)
|
|
14
|
+
- Finance Agent: Can use calculator, file_reader (reports only)
|
|
15
|
+
- Admin Agent: Can use admin tools (restricted access)
|
|
16
|
+
|
|
17
|
+
Prerequisites:
|
|
18
|
+
- MACAW SDK installed (pip install macaw-client macaw-adapters)
|
|
19
|
+
- OPENAI_API_KEY environment variable (for ChatOpenAI)
|
|
20
|
+
|
|
21
|
+
Run:
|
|
22
|
+
export OPENAI_API_KEY=sk-...
|
|
23
|
+
python langchain_1c_orchestration.py
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
import os
|
|
27
|
+
from typing import Dict, Any
|
|
28
|
+
|
|
29
|
+
from macaw_adapters.langchain import create_react_agent, AgentExecutor, cleanup
|
|
30
|
+
|
|
31
|
+
from langchain_openai import ChatOpenAI
|
|
32
|
+
from langchain.tools import Tool
|
|
33
|
+
from langchain.prompts import PromptTemplate
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# Define specialized tools for different agents
|
|
37
|
+
def calculator(expression: str) -> str:
|
|
38
|
+
"""Evaluate a math expression."""
|
|
39
|
+
try:
|
|
40
|
+
result = eval(expression)
|
|
41
|
+
return f"Result: {result}"
|
|
42
|
+
except Exception as e:
|
|
43
|
+
return f"Error: {e}"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def search_web(query: str) -> str:
|
|
47
|
+
"""Search the web (mock)."""
|
|
48
|
+
return f"Search results for '{query}': [Top 3 relevant articles about {query}]"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def file_reader(path: str) -> str:
|
|
52
|
+
"""Read a file."""
|
|
53
|
+
if "report" in path.lower():
|
|
54
|
+
return "Q4 Revenue: $1.2M, Growth: 15%, Expenses: $800K"
|
|
55
|
+
elif "public" in path.lower():
|
|
56
|
+
return "Public announcement: Company expanding to new markets"
|
|
57
|
+
elif "secret" in path.lower() or "confidential" in path.lower():
|
|
58
|
+
return "ACCESS DENIED - Classified content"
|
|
59
|
+
return f"Contents of {path}: [document data]"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def admin_tool(command: str) -> str:
|
|
63
|
+
"""Execute admin command."""
|
|
64
|
+
return f"Admin executed: {command}"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def send_email(input_str: str) -> str:
|
|
68
|
+
"""Send an email (mock). Input format: 'to@email.com: subject text'"""
|
|
69
|
+
# Parse simple format: "to: subject" or just treat whole thing as recipient
|
|
70
|
+
if ":" in input_str:
|
|
71
|
+
parts = input_str.split(":", 1)
|
|
72
|
+
to = parts[0].strip()
|
|
73
|
+
subject = parts[1].strip() if len(parts) > 1 else "No subject"
|
|
74
|
+
else:
|
|
75
|
+
to = input_str.strip()
|
|
76
|
+
subject = "Notification"
|
|
77
|
+
return f"Email sent to {to}: {subject}"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# All available tools
|
|
81
|
+
ALL_TOOLS = [
|
|
82
|
+
Tool(name="calculator", func=calculator, description="Math calculations"),
|
|
83
|
+
Tool(name="search", func=search_web, description="Search the web for information"),
|
|
84
|
+
Tool(name="file_reader", func=file_reader, description="Read files"),
|
|
85
|
+
Tool(name="admin", func=admin_tool, description="Execute admin commands"),
|
|
86
|
+
Tool(name="email", func=send_email, description="Send emails"),
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# ReAct prompt template
|
|
91
|
+
REACT_PROMPT = PromptTemplate.from_template("""Answer the following questions as best you can. You have access to the following tools:
|
|
92
|
+
|
|
93
|
+
{tools}
|
|
94
|
+
|
|
95
|
+
Use the following format:
|
|
96
|
+
|
|
97
|
+
Question: the input question you must answer
|
|
98
|
+
Thought: you should always think about what to do
|
|
99
|
+
Action: the action to take, should be one of [{tool_names}]
|
|
100
|
+
Action Input: the input to the action
|
|
101
|
+
Observation: the result of the action
|
|
102
|
+
... (this Thought/Action/Action Input/Observation can repeat N times)
|
|
103
|
+
Thought: I now know the final answer
|
|
104
|
+
Final Answer: the final answer to the original input question
|
|
105
|
+
|
|
106
|
+
Begin!
|
|
107
|
+
|
|
108
|
+
Question: {input}
|
|
109
|
+
Thought:{agent_scratchpad}""")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# Specialized agent configurations with MAPL format policies
|
|
113
|
+
AGENT_CONFIGS = {
|
|
114
|
+
"research": {
|
|
115
|
+
"description": "Research Agent - web search and public documents",
|
|
116
|
+
"policy": {
|
|
117
|
+
"resources": ["tool:search", "tool:file_reader"],
|
|
118
|
+
"denied_resources": ["tool:admin", "tool:calculator", "tool:email"],
|
|
119
|
+
"constraints": {
|
|
120
|
+
"parameters": {
|
|
121
|
+
"tool:file_reader": {"input": ["*public*"]}
|
|
122
|
+
},
|
|
123
|
+
"denied_parameters": {
|
|
124
|
+
"tool:*": {"input": ["*password*", "*secret*", "*confidential*"]}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
"finance": {
|
|
130
|
+
"description": "Finance Agent - calculations and financial reports",
|
|
131
|
+
"policy": {
|
|
132
|
+
"resources": ["tool:calculator", "tool:file_reader"],
|
|
133
|
+
"denied_resources": ["tool:admin", "tool:search", "tool:email"],
|
|
134
|
+
"constraints": {
|
|
135
|
+
"parameters": {
|
|
136
|
+
"tool:file_reader": {"input": ["*report*", "*forecast*", "*budget*"]}
|
|
137
|
+
},
|
|
138
|
+
"denied_parameters": {
|
|
139
|
+
"tool:*": {"input": ["*password*"]}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
"admin": {
|
|
145
|
+
"description": "Admin Agent - system administration (restricted)",
|
|
146
|
+
"policy": {
|
|
147
|
+
"resources": ["tool:admin", "tool:email"],
|
|
148
|
+
"denied_resources": ["tool:calculator", "tool:search", "tool:file_reader"],
|
|
149
|
+
"constraints": {
|
|
150
|
+
"denied_parameters": {
|
|
151
|
+
"tool:*": {"input": ["*delete*", "*drop*", "*truncate*"]}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def create_specialized_agent(name: str, config: Dict[str, Any], llm) -> AgentExecutor:
|
|
160
|
+
"""Create a specialized agent with its security policy."""
|
|
161
|
+
agent = create_react_agent(
|
|
162
|
+
llm=llm,
|
|
163
|
+
tools=ALL_TOOLS,
|
|
164
|
+
prompt=REACT_PROMPT,
|
|
165
|
+
security_policy=config["policy"]
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
return AgentExecutor(
|
|
169
|
+
agent=agent,
|
|
170
|
+
tools=ALL_TOOLS,
|
|
171
|
+
security_policy=config["policy"],
|
|
172
|
+
verbose=False,
|
|
173
|
+
handle_parsing_errors=True,
|
|
174
|
+
max_iterations=3
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class SupervisorAgent:
|
|
179
|
+
"""
|
|
180
|
+
Supervisor that routes tasks to appropriate specialized agents.
|
|
181
|
+
|
|
182
|
+
Key security concept: The supervisor can only delegate to agents
|
|
183
|
+
with equal or more restrictive policies. It cannot grant permissions
|
|
184
|
+
it doesn't have (policy intersection).
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
def __init__(self, llm):
|
|
188
|
+
self.llm = llm
|
|
189
|
+
self.agents: Dict[str, AgentExecutor] = {}
|
|
190
|
+
|
|
191
|
+
# Create specialized agents
|
|
192
|
+
print("\n Creating specialized agents:")
|
|
193
|
+
for name, config in AGENT_CONFIGS.items():
|
|
194
|
+
print(f" - {name}: {config['description']}")
|
|
195
|
+
self.agents[name] = create_specialized_agent(name, config, llm)
|
|
196
|
+
|
|
197
|
+
def route(self, query: str) -> str:
|
|
198
|
+
"""Route query to appropriate agent based on content."""
|
|
199
|
+
query_lower = query.lower()
|
|
200
|
+
|
|
201
|
+
# Simple routing logic (in production, could use LLM for routing)
|
|
202
|
+
if any(word in query_lower for word in ["calculate", "math", "revenue", "profit", "cost", "budget"]):
|
|
203
|
+
return "finance"
|
|
204
|
+
elif any(word in query_lower for word in ["search", "find", "lookup", "research", "public"]):
|
|
205
|
+
return "research"
|
|
206
|
+
elif any(word in query_lower for word in ["admin", "restart", "status", "notify", "email"]):
|
|
207
|
+
return "admin"
|
|
208
|
+
else:
|
|
209
|
+
return "research" # Default to research
|
|
210
|
+
|
|
211
|
+
def invoke(self, query: str) -> Dict[str, Any]:
|
|
212
|
+
"""Route and execute query through appropriate agent."""
|
|
213
|
+
agent_name = self.route(query)
|
|
214
|
+
agent = self.agents[agent_name]
|
|
215
|
+
|
|
216
|
+
print(f" Routing to: {agent_name}")
|
|
217
|
+
print(f" Policy: {AGENT_CONFIGS[agent_name]['policy'].get('resources', 'all')}")
|
|
218
|
+
|
|
219
|
+
try:
|
|
220
|
+
result = agent.invoke({"input": query})
|
|
221
|
+
return {
|
|
222
|
+
"routed_to": agent_name,
|
|
223
|
+
"success": True,
|
|
224
|
+
"output": result.get("output", str(result))
|
|
225
|
+
}
|
|
226
|
+
except Exception as e:
|
|
227
|
+
error_msg = str(e).lower()
|
|
228
|
+
if any(word in error_msg for word in ["denied", "blocked", "policy", "not allowed"]):
|
|
229
|
+
return {
|
|
230
|
+
"routed_to": agent_name,
|
|
231
|
+
"success": False,
|
|
232
|
+
"blocked": True,
|
|
233
|
+
"output": f"Policy blocked: {str(e)[:80]}"
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
"routed_to": agent_name,
|
|
237
|
+
"success": False,
|
|
238
|
+
"output": f"Error: {str(e)[:80]}"
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def main():
|
|
243
|
+
if not os.environ.get("OPENAI_API_KEY"):
|
|
244
|
+
print("Set OPENAI_API_KEY environment variable")
|
|
245
|
+
return
|
|
246
|
+
|
|
247
|
+
print("=" * 60)
|
|
248
|
+
print("Example 1c: Agent Orchestration (LangChain)")
|
|
249
|
+
print("=" * 60)
|
|
250
|
+
print("\nPattern: Supervisor routes to specialized sub-agents")
|
|
251
|
+
print("Use when: Multi-agent workflows with different permission levels")
|
|
252
|
+
print("\nArchitecture:")
|
|
253
|
+
print(" Supervisor --> Research Agent (search, public files)")
|
|
254
|
+
print(" --> Finance Agent (calculator, reports)")
|
|
255
|
+
print(" --> Admin Agent (admin tools, email)")
|
|
256
|
+
|
|
257
|
+
# Shared LLM
|
|
258
|
+
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
|
|
259
|
+
|
|
260
|
+
# Create supervisor
|
|
261
|
+
print("\n--- Creating Supervisor Agent ---")
|
|
262
|
+
supervisor = SupervisorAgent(llm)
|
|
263
|
+
|
|
264
|
+
# Test cases demonstrating routing and policy enforcement
|
|
265
|
+
TEST_CASES = [
|
|
266
|
+
# Should route to finance, use calculator
|
|
267
|
+
("Calculate the profit if revenue is 1200000 and expenses are 800000", "finance", True),
|
|
268
|
+
|
|
269
|
+
# Should route to research, use search
|
|
270
|
+
("Search for public information about AI trends", "research", True),
|
|
271
|
+
|
|
272
|
+
# Should route to admin, use email
|
|
273
|
+
("Send an email to team@company.com about the system status", "admin", True),
|
|
274
|
+
|
|
275
|
+
# Should route to finance, but try to access secret file (blocked - not in allowed patterns)
|
|
276
|
+
("What is the profit in the secret internal file?", "finance", False),
|
|
277
|
+
|
|
278
|
+
# Should route to research, but research agent can't use calculator
|
|
279
|
+
("Search for how to compute 100 * 50", "research", True), # Research will search, not calculate
|
|
280
|
+
]
|
|
281
|
+
|
|
282
|
+
print("\n" + "=" * 60)
|
|
283
|
+
print("Running test cases")
|
|
284
|
+
print("=" * 60)
|
|
285
|
+
|
|
286
|
+
for query, expected_agent, should_succeed in TEST_CASES:
|
|
287
|
+
print(f"\n Query: {query}")
|
|
288
|
+
print(f" Expected: routes to '{expected_agent}', {'succeeds' if should_succeed else 'blocked'}")
|
|
289
|
+
|
|
290
|
+
result = supervisor.invoke(query)
|
|
291
|
+
|
|
292
|
+
routed = result["routed_to"]
|
|
293
|
+
succeeded = result["success"]
|
|
294
|
+
output = result["output"][:60] + "..." if len(result.get("output", "")) > 60 else result.get("output", "")
|
|
295
|
+
|
|
296
|
+
# Check if routing was correct
|
|
297
|
+
route_correct = routed == expected_agent
|
|
298
|
+
outcome_correct = succeeded == should_succeed
|
|
299
|
+
|
|
300
|
+
status = "PASS" if (route_correct and outcome_correct) else "UNEXPECTED"
|
|
301
|
+
|
|
302
|
+
print(f" Result: [{status}] Routed to '{routed}', {'succeeded' if succeeded else 'blocked/failed'}")
|
|
303
|
+
print(f" Output: {output}")
|
|
304
|
+
|
|
305
|
+
# Cleanup
|
|
306
|
+
print("\n--- Cleanup ---")
|
|
307
|
+
cleanup()
|
|
308
|
+
|
|
309
|
+
print("\n" + "=" * 60)
|
|
310
|
+
print("Example complete!")
|
|
311
|
+
print("=" * 60)
|
|
312
|
+
print("\nKey concepts demonstrated:")
|
|
313
|
+
print(" - Supervisor pattern for agent orchestration")
|
|
314
|
+
print(" - Each sub-agent has isolated security policy")
|
|
315
|
+
print(" - Policy enforcement at tool execution time")
|
|
316
|
+
print(" - Routing based on task type")
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
if __name__ == "__main__":
|
|
320
|
+
try:
|
|
321
|
+
main()
|
|
322
|
+
except Exception as e:
|
|
323
|
+
err = str(e)
|
|
324
|
+
print("\n" + "=" * 60)
|
|
325
|
+
if "OPENAI_API_KEY" in err or "api_key" in err.lower():
|
|
326
|
+
print("ERROR: OpenAI API key not configured")
|
|
327
|
+
print("Fix: export OPENAI_API_KEY=sk-...")
|
|
328
|
+
else:
|
|
329
|
+
print(f"ERROR: {e}")
|
|
330
|
+
print("=" * 60)
|
|
331
|
+
import sys
|
|
332
|
+
sys.exit(1)
|