@macaw-ai/client 0.0.0-stage → 0.9.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/LICENSE.md +339 -0
  2. package/bin/install.js +278 -0
  3. package/package.json +37 -4
  4. package/payload/README.md +56 -0
  5. package/payload/secureAI/LICENSE +190 -0
  6. package/payload/secureAI/NOTICE +5 -0
  7. package/payload/secureAI/demos/policy-controls/1_uncontrolled.py +139 -0
  8. package/payload/secureAI/demos/policy-controls/2_with_macaw.py +139 -0
  9. package/payload/secureAI/demos/policy-controls/3_with_user_policies.py +162 -0
  10. package/payload/secureAI/demos/policy-controls/credentials.txt +12 -0
  11. package/payload/secureAI/demos/policy-controls/malicious_report.txt +21 -0
  12. package/payload/secureAI/demos/policy-controls/sales_report.txt +20 -0
  13. package/payload/secureAI/demos/tutorial-1-rbac/README.md +143 -0
  14. package/payload/secureAI/demos/tutorial-1-rbac/app.py +147 -0
  15. package/payload/secureAI/demos/tutorial-1-rbac/authprompts_demo.py +306 -0
  16. package/payload/secureAI/demos/tutorial-1-rbac/config/claims-config.yaml +152 -0
  17. package/payload/secureAI/demos/tutorial-1-rbac/demo_secureopenai_with_policies.py +150 -0
  18. package/payload/secureAI/demos/tutorial-1-rbac/policies/bu_Analytics.json +8 -0
  19. package/payload/secureAI/demos/tutorial-1-rbac/policies/company_FinTech_Corp.json +70 -0
  20. package/payload/secureAI/demos/tutorial-1-rbac/policies/llm-base.json +69 -0
  21. package/payload/secureAI/demos/tutorial-1-rbac/policies/team_Reporting.json +8 -0
  22. package/payload/secureAI/demos/tutorial-1-rbac/policies/user_alice.json +32 -0
  23. package/payload/secureAI/demos/tutorial-1-rbac/policies/user_bob.json +28 -0
  24. package/payload/secureAI/demos/tutorial-1-rbac/policies/user_carol.json +34 -0
  25. package/payload/secureAI/demos/tutorial-1-rbac/requirements.txt +6 -0
  26. package/payload/secureAI/demos/tutorial-1-rbac/setup/auth0_complete_setup.sh +392 -0
  27. package/payload/secureAI/demos/tutorial-1-rbac/setup/keycloak_complete_setup.sh +320 -0
  28. package/payload/secureAI/demos/tutorial-2-intent/Policies/alation-remote-proxy.json +155 -0
  29. package/payload/secureAI/demos/tutorial-2-intent/Policies/bu.json +22 -0
  30. package/payload/secureAI/demos/tutorial-2-intent/Policies/company.json +17 -0
  31. package/payload/secureAI/demos/tutorial-2-intent/Policies/user_alice.json +48 -0
  32. package/payload/secureAI/demos/tutorial-2-intent/Policies/user_bob.json +83 -0
  33. package/payload/secureAI/demos/tutorial-2-intent/README.md +213 -0
  34. package/payload/secureAI/demos/tutorial-2-intent/script.py +188 -0
  35. package/payload/secureAI/demos/tutorial-2-intent/setup/alation_verifier.py +177 -0
  36. package/payload/secureAI/demos/tutorial-2-intent/setup/get_alation_user_token.py +180 -0
  37. package/payload/secureAI/examples/README.md +244 -0
  38. package/payload/secureAI/examples/anthropic/anthropic_1a_dropin_simple.py +103 -0
  39. package/payload/secureAI/examples/anthropic/anthropic_1b_multiuser_bind.py +190 -0
  40. package/payload/secureAI/examples/anthropic/anthropic_1b_multiuser_bind_streaming.py +190 -0
  41. package/payload/secureAI/examples/anthropic/anthropic_1c_a2a_invoke.py +156 -0
  42. package/payload/secureAI/examples/attestations/1a_trade_alice.py +215 -0
  43. package/payload/secureAI/examples/attestations/1a_trade_bob.py +164 -0
  44. package/payload/secureAI/examples/attestations/1b_grant_admin.py +170 -0
  45. package/payload/secureAI/examples/attestations/1b_grant_researcher.py +231 -0
  46. package/payload/secureAI/examples/attestations/README.md +128 -0
  47. package/payload/secureAI/examples/langchain/langchain_1a_dropin_simple.py +199 -0
  48. package/payload/secureAI/examples/langchain/langchain_1b_multiuser.py +262 -0
  49. package/payload/secureAI/examples/langchain/langchain_1c_orchestration.py +332 -0
  50. package/payload/secureAI/examples/langchain/langchain_1d_llm_openai.py +130 -0
  51. package/payload/secureAI/examples/langchain/langchain_1e_llm_anthropic.py +130 -0
  52. package/payload/secureAI/examples/langchain/langchain_1f_memory.py +214 -0
  53. package/payload/secureAI/examples/litellm/litellm_1a_dropin_simple.py +131 -0
  54. package/payload/secureAI/examples/mcp/1a_simple_invocation.py +115 -0
  55. package/payload/secureAI/examples/mcp/1b_discovery_and_resources.py +142 -0
  56. package/payload/secureAI/examples/mcp/1c_logging_client.py +100 -0
  57. package/payload/secureAI/examples/mcp/1d_progress_client.py +100 -0
  58. package/payload/secureAI/examples/mcp/1e_sampling_client.py +150 -0
  59. package/payload/secureAI/examples/mcp/1e_sampling_server.py +90 -0
  60. package/payload/secureAI/examples/mcp/1f_elicitation_client.py +127 -0
  61. package/payload/secureAI/examples/mcp/1f_elicitation_server.py +73 -0
  62. package/payload/secureAI/examples/mcp/1g_roots_client.py +122 -0
  63. package/payload/secureAI/examples/mcp/1g_roots_server.py +85 -0
  64. package/payload/secureAI/examples/mcp/2a_proxy_snowflake.py +212 -0
  65. package/payload/secureAI/examples/mcp/2b_proxy_databricks.py +299 -0
  66. package/payload/secureAI/examples/mcp/2c_proxy_stdio_fetch.py +91 -0
  67. package/payload/secureAI/examples/mcp/2d_proxy_stdio_salesforce.py +119 -0
  68. package/payload/secureAI/examples/mcp/2e_proxy_multiuser.py +136 -0
  69. package/payload/secureAI/examples/mcp/2f_proxy_stdio_databricks.py +144 -0
  70. package/payload/secureAI/examples/mcp/3a_native_mcp_client.py +104 -0
  71. package/payload/secureAI/examples/mcp/3b_native_mcp_proxy.py +114 -0
  72. package/payload/secureAI/examples/mcp/3c_native_mcp_sampling.py +180 -0
  73. package/payload/secureAI/examples/mcp/3d_native_mcp_2026.py +135 -0
  74. package/payload/secureAI/examples/mcp/securemcp_calculator.py +248 -0
  75. package/payload/secureAI/examples/mcp/securemcp_server.py +75 -0
  76. package/payload/secureAI/examples/openai/openai_1a_dropin_simple.py +102 -0
  77. package/payload/secureAI/examples/openai/openai_1b_multiuser_bind.py +193 -0
  78. package/payload/secureAI/examples/openai/openai_1b_multiuser_bind_streaming.py +199 -0
  79. package/payload/secureAI/examples/openai/openai_1c_a2a_invoke.py +156 -0
  80. package/payload/secureAI/examples/openai/openai_1d_invocation_patterns.py +337 -0
  81. package/payload/secureAI/examples/phishing-resistance/1a_login_alice.py +180 -0
  82. package/payload/secureAI/examples/phishing-resistance/1b_trade_alice.py +241 -0
  83. package/payload/secureAI/examples/phishing-resistance/1b_trade_bob.py +164 -0
  84. package/payload/secureAI/examples/phishing-resistance/README.md +124 -0
  85. package/payload/secureAI/examples/phishing-resistance/get_token.py +114 -0
  86. package/payload/secureAI/examples/pydantic_ai/README.md +88 -0
  87. package/payload/secureAI/examples/pydantic_ai/policies/app_pydantic-agent.json +10 -0
  88. package/payload/secureAI/examples/pydantic_ai/pydanticai_1a_dropin_simple.py +90 -0
  89. package/payload/secureAI/examples/pydantic_ai/pydanticai_1b_model_routing.py +85 -0
  90. package/payload/secureAI/examples/pydantic_ai/pydanticai_1c_native_tools.py +83 -0
  91. package/payload/secureAI/examples/pydantic_ai/pydanticai_1d_multiuser_bind.py +104 -0
  92. package/payload/secureAI/examples/pydantic_ai/pydanticai_1e_mcp_compose.py +92 -0
  93. package/payload/secureAI/macaw_adapters/__init__.py +55 -0
  94. package/payload/secureAI/macaw_adapters/anthropic/REFERENCE.md +374 -0
  95. package/payload/secureAI/macaw_adapters/anthropic/__init__.py +22 -0
  96. package/payload/secureAI/macaw_adapters/anthropic/secure_anthropic.py +792 -0
  97. package/payload/secureAI/macaw_adapters/anthropic_ext_tools/__init__.py +25 -0
  98. package/payload/secureAI/macaw_adapters/anthropic_ext_tools/secure_anthropic.py +840 -0
  99. package/payload/secureAI/macaw_adapters/langchain/REFERENCE.md +234 -0
  100. package/payload/secureAI/macaw_adapters/langchain/__init__.py +100 -0
  101. package/payload/secureAI/macaw_adapters/langchain/_utils.py +83 -0
  102. package/payload/secureAI/macaw_adapters/langchain/agents.py +296 -0
  103. package/payload/secureAI/macaw_adapters/langchain/anthropic.py +456 -0
  104. package/payload/secureAI/macaw_adapters/langchain/callbacks.py +306 -0
  105. package/payload/secureAI/macaw_adapters/langchain/memory.py +352 -0
  106. package/payload/secureAI/macaw_adapters/langchain/openai.py +410 -0
  107. package/payload/secureAI/macaw_adapters/langchain/tools.py +174 -0
  108. package/payload/secureAI/macaw_adapters/litellm/README.md +216 -0
  109. package/payload/secureAI/macaw_adapters/litellm/__init__.py +143 -0
  110. package/payload/secureAI/macaw_adapters/litellm/secure_litellm.py +686 -0
  111. package/payload/secureAI/macaw_adapters/mcp/REFERENCE.md +460 -0
  112. package/payload/secureAI/macaw_adapters/mcp/__init__.py +65 -0
  113. package/payload/secureAI/macaw_adapters/mcp/_endpoint.py +193 -0
  114. package/payload/secureAI/macaw_adapters/mcp/_endpoint2.py +225 -0
  115. package/payload/secureAI/macaw_adapters/mcp/_serve.py +25 -0
  116. package/payload/secureAI/macaw_adapters/mcp/client.py +721 -0
  117. package/payload/secureAI/macaw_adapters/mcp/decorators.py +228 -0
  118. package/payload/secureAI/macaw_adapters/mcp/fastmcp_compat.py +353 -0
  119. package/payload/secureAI/macaw_adapters/mcp/mcp.py +785 -0
  120. package/payload/secureAI/macaw_adapters/mcp/proxy.py +626 -0
  121. package/payload/secureAI/macaw_adapters/mcp/server.py +317 -0
  122. package/payload/secureAI/macaw_adapters/mcp/subscriptions.py +450 -0
  123. package/payload/secureAI/macaw_adapters/openai/REFERENCE.md +368 -0
  124. package/payload/secureAI/macaw_adapters/openai/__init__.py +22 -0
  125. package/payload/secureAI/macaw_adapters/openai/secure_openai.py +833 -0
  126. package/payload/secureAI/macaw_adapters/openai_ext_tools/REFERENCE.md +368 -0
  127. package/payload/secureAI/macaw_adapters/openai_ext_tools/__init__.py +25 -0
  128. package/payload/secureAI/macaw_adapters/openai_ext_tools/secure_openai.py +876 -0
  129. package/payload/secureAI/macaw_adapters/pydantic_ai/__init__.py +36 -0
  130. package/payload/secureAI/macaw_adapters/pydantic_ai/_core.py +257 -0
  131. package/payload/secureAI/macaw_adapters/pydantic_ai/agent.py +218 -0
  132. package/payload/secureAI/macaw_adapters/pydantic_ai/model.py +99 -0
  133. package/payload/secureAI/macaw_adapters/pydantic_ai/toolset.py +86 -0
  134. package/payload/secureAI/test_harness_quick.py +296 -0
  135. package/wheels/macaw_adapters-0.9.10.1-py3-none-any.whl +0 -0
  136. package/wheels/macaw_client-0.9.10.1-cp312-cp312-macosx_11_0_arm64.whl +0 -0
  137. package/wheels/macaw_client-0.9.10.1-cp312-cp312-manylinux_2_17_x86_64.whl +0 -0
  138. package/wheels/macaw_client-0.9.10.1-cp314-cp314-macosx_11_0_arm64.whl +0 -0
  139. package/wheels/macaw_client-0.9.10.1-cp314-cp314-manylinux_2_17_x86_64.whl +0 -0
  140. package/wheels/macaw_client-0.9.10.1-cp314-cp314-win_amd64.whl +0 -0
  141. package/wheels/macaw_client-0.9.10.1-cp39-cp39-macosx_11_0_arm64.whl +0 -0
  142. package/README.md +0 -3
@@ -0,0 +1,686 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ SecureLiteLLM - MACAW-protected LiteLLM wrapper.
4
+
5
+ Drop-in replacement for litellm with MACAW security.
6
+ Supports 100+ LLM providers through LiteLLM's unified interface.
7
+ Uses MAPL-compliant tool names: tool:<app_name>/generate, tool:<app_name>/complete, etc.
8
+
9
+ Supports three usage paths:
10
+ 1. Direct on service: client.chat.completions.create() - simplest, app-level identity
11
+ 2. bind_to_user: service.bind_to_user(user_client) - per-user identity for SaaS
12
+ 3. invoke_tool: user.invoke_tool("tool:xxx/generate", ...) - explicit A2A control
13
+
14
+ Architecture (service mode):
15
+ - macaw_client: Primary client owning LLM operations (tool:{app_name}/generate, etc.)
16
+ - _tools_client: Internal client owning user-registered tools (tool:{app_name}/*, not externally reachable)
17
+ - server_id: Points to macaw_client (the primary interface)
18
+ - Tool isolation: Only macaw_client can invoke tools on _tools_client
19
+
20
+ Install with: pip install -e /path/to/secureAI[litellm]
21
+ """
22
+
23
+ import os
24
+ import json
25
+ import logging
26
+ import inspect
27
+ from typing import Dict, Any, Optional, Callable, List
28
+
29
+ import litellm
30
+ from macaw_client import MACAWClient
31
+
32
+ logger = logging.getLogger(__name__)
33
+
34
+ # MAPL-compliant resource naming: tool:<service>/<operation>
35
+ TOOL_OPERATIONS = {
36
+ "generate": "generate",
37
+ "complete": "complete",
38
+ "embed": "embed"
39
+ }
40
+
41
+
42
+ class SecureLiteLLM:
43
+ """
44
+ Drop-in replacement for litellm with MACAW security.
45
+
46
+ This version uses a two-client architecture for tool isolation:
47
+ - macaw_client: Primary, owns LLM operations (tool:{app_name}/generate, etc.)
48
+ - _tools_client: Internal, owns user-registered tools (not externally reachable)
49
+
50
+ server_id points to macaw_client, making it the primary external interface.
51
+ Tools are isolated: only macaw_client can invoke tools on _tools_client.
52
+
53
+ Supports two modes:
54
+ 1. Service mode (default): Creates both clients, handles LLM calls
55
+ 2. User mode (jwt_token provided): Creates user agent with identity for direct calls
56
+
57
+ For multi-user scenarios, use service mode + bind_to_user() to create per-user wrappers.
58
+ """
59
+
60
+ def __init__(
61
+ self,
62
+ app_name: str = None,
63
+ api_base: str = None,
64
+ api_key: str = None,
65
+ intent_policy: Optional[Dict[str, Any]] = None,
66
+ # User mode parameters
67
+ jwt_token: str = None,
68
+ user_name: str = None
69
+ ):
70
+ """
71
+ Initialize SecureLiteLLM wrapper.
72
+
73
+ Args:
74
+ app_name: Application name for registration (default: macaw-litellm)
75
+ api_base: Optional custom API endpoint (for vLLM, Ollama, etc.)
76
+ api_key: Optional API key (LiteLLM uses env vars per provider by default)
77
+ intent_policy: Application-defined MACAW intent policy
78
+ jwt_token: If provided, creates user agent with this identity (user mode)
79
+ user_name: Optional user name for user mode
80
+ """
81
+ # Store LiteLLM config
82
+ self.api_base = api_base
83
+ self.api_key = api_key
84
+
85
+ # Application identity
86
+ self.app_name = app_name or "macaw-litellm"
87
+
88
+ # Determine mode based on jwt_token
89
+ self._mode = "user" if jwt_token else "service"
90
+ self._jwt_token = jwt_token
91
+ self._user_name = user_name
92
+
93
+ # User-registered tools (LLM can call these)
94
+ self.user_tools = {}
95
+
96
+ # Auto-discovered tools cache
97
+ self._discovered_tools = {}
98
+ self._tools_registered = False
99
+
100
+ # Application-provided intent policy (no defaults!)
101
+ self.intent_policy = intent_policy or {}
102
+
103
+ if self._mode == "service":
104
+ # SERVICE MODE: Two-client architecture for tool isolation
105
+ # - macaw_client: Primary, owns LLM operations (externally reachable)
106
+ # - _tools_client: Internal, owns user tools (not externally reachable)
107
+
108
+ # Tools client name (same as app_name for cleaner policy names)
109
+ self._tools_name = self.app_name
110
+
111
+ # LLM tools on the primary client
112
+ llm_tools = {
113
+ f"tool:{self.app_name}/generate": {
114
+ "handler": self._handle_generate,
115
+ "prompts": ["messages"]
116
+ },
117
+ f"tool:{self.app_name}/complete": {
118
+ "handler": self._handle_complete,
119
+ "prompts": ["prompt"]
120
+ },
121
+ f"tool:{self.app_name}/embed": {
122
+ "handler": self._handle_embed,
123
+ "prompts": ["input"]
124
+ }
125
+ }
126
+
127
+ # Primary client - owns LLM operations, externally reachable via server_id
128
+ self.macaw_client = MACAWClient(
129
+ app_name=self.app_name,
130
+ app_version="1.0.0",
131
+ intent_policy=self.intent_policy,
132
+ tools=llm_tools
133
+ )
134
+
135
+ # Internal tools client - owns user-registered tools, NOT externally reachable
136
+ self._tools_client = MACAWClient(
137
+ app_name=self._tools_name,
138
+ app_version="1.0.0",
139
+ intent_policy=self.intent_policy,
140
+ tools={} # User tools added via register_tool
141
+ )
142
+ else:
143
+ # USER MODE: Create user agent with identity
144
+ self._tools_name = None
145
+ self._tools_client = None
146
+
147
+ self.macaw_client = MACAWClient(
148
+ user_name=user_name,
149
+ iam_token=jwt_token,
150
+ agent_type="user",
151
+ app_name=self.app_name,
152
+ app_version="1.0.0",
153
+ intent_policy=self.intent_policy or {
154
+ "purpose": f"LiteLLM access for {user_name or 'user'}"
155
+ }
156
+ )
157
+
158
+ # Register with LocalAgent and get server ID(s)
159
+ if self._mode == "service":
160
+ # Register both clients
161
+ if self.macaw_client.register() and self._tools_client.register():
162
+ self.server_id = self.macaw_client.agent_id # Primary interface!
163
+ self._tools_server_id = self._tools_client.agent_id # Internal, not exposed
164
+ logger.info(f"SecureLiteLLM registered (two-client mode):")
165
+ logger.info(f" Primary (LLM): {self.server_id}")
166
+ logger.info(f" Internal (tools): {self._tools_server_id}")
167
+ else:
168
+ raise RuntimeError("Failed to register with MACAW LocalAgent")
169
+ else:
170
+ # User mode - single client
171
+ if self.macaw_client.register():
172
+ self.server_id = self.macaw_client.agent_id
173
+ logger.info(f"SecureLiteLLM registered as: {self.server_id} (mode: {self._mode})")
174
+ else:
175
+ raise RuntimeError("Failed to register with MACAW LocalAgent")
176
+
177
+ # Mimic OpenAI API structure for compatibility
178
+ self.chat = self._ChatNamespace(self)
179
+ self.completions = self._CompletionsNamespace(self)
180
+ self.embeddings = self._EmbeddingsNamespace(self)
181
+
182
+ def completion(self, model: str, messages: List[Dict], **kwargs):
183
+ """
184
+ Direct completion call - convenience method.
185
+
186
+ This is the primary interface matching litellm.completion().
187
+ """
188
+ return self.chat.completions.create(model=model, messages=messages, **kwargs)
189
+
190
+ def bind_to_user(self, user_client: 'MACAWClient') -> 'BoundSecureLiteLLM':
191
+ """
192
+ Bind this SecureLiteLLM service to a user's MACAW client.
193
+
194
+ Creates a lightweight wrapper that routes calls through the user's
195
+ client to this service, enabling per-user identity and policy enforcement.
196
+
197
+ Only valid in service mode.
198
+
199
+ Args:
200
+ user_client: A registered MACAWClient with user identity
201
+
202
+ Returns:
203
+ BoundSecureLiteLLM wrapper for this user
204
+
205
+ Raises:
206
+ ValueError: If service not in service mode or client validation fails
207
+ """
208
+ if self._mode != "service":
209
+ raise ValueError("bind_to_user() only valid for service-mode SecureLiteLLM")
210
+
211
+ # Validate user_client is a MACAWClient instance
212
+ if not hasattr(user_client, 'agent_id') or not hasattr(user_client, 'invoke_tool'):
213
+ raise ValueError("bind_to_user() requires a valid MACAWClient instance")
214
+
215
+ # Validate user_client is registered
216
+ if not getattr(user_client, 'registered', False):
217
+ raise ValueError("bind_to_user() requires a registered MACAWClient. Call register() first.")
218
+
219
+ # Validate agent_type is "user" (warning only, not blocking)
220
+ agent_type = getattr(user_client, 'agent_type', None)
221
+ if agent_type and agent_type != "user":
222
+ logger.warning(f"bind_to_user() called with agent_type='{agent_type}' (expected 'user'). "
223
+ f"User identity and policy enforcement may not work as expected.")
224
+
225
+ return BoundSecureLiteLLM(self, user_client)
226
+
227
+ def register_tool(self, name: str, handler: Callable) -> 'SecureLiteLLM':
228
+ """
229
+ Register a tool that the LLM can call.
230
+
231
+ Tools are registered on _tools_client (internal, not externally reachable).
232
+ This provides tool isolation - only the LLM can invoke these tools.
233
+
234
+ Args:
235
+ name: Tool name (must match function name in tools list)
236
+ handler: Function to handle tool execution
237
+
238
+ Returns:
239
+ Self for chaining
240
+ """
241
+ # Store with MAPL-compliant name using tools client's app_name
242
+ mapl_name = f"tool:{self._tools_name}/{name}"
243
+
244
+ # Wrap handler to accept params dict and unpack as kwargs
245
+ def wrapped_handler(params):
246
+ return handler(**params)
247
+
248
+ self.user_tools[name] = wrapped_handler # Store by simple name for lookup
249
+ self.user_tools[mapl_name] = wrapped_handler # Also store by MAPL name
250
+
251
+ # Register with _tools_client (internal) using MAPL name
252
+ self._tools_client.register_tool(mapl_name, wrapped_handler)
253
+
254
+ logger.info(f"Registered tool: {name} -> {mapl_name}")
255
+ return self
256
+
257
+ def _auto_discover_tools(self, tools_metadata):
258
+ """
259
+ Auto-discover tool implementations from caller's context.
260
+
261
+ Args:
262
+ tools_metadata: List of tool definitions from LLM call
263
+ """
264
+ if self._tools_registered:
265
+ return # Already discovered
266
+
267
+ # Get caller's frame
268
+ frame = inspect.currentframe()
269
+ caller_frame = frame.f_back.f_back.f_back
270
+ if not caller_frame:
271
+ logger.warning("Could not access caller frame for auto-discovery")
272
+ return
273
+
274
+ caller_globals = caller_frame.f_globals
275
+ caller_locals = caller_frame.f_locals
276
+
277
+ # Combine locals and globals for search
278
+ search_scope = {**caller_globals, **caller_locals}
279
+
280
+ # Discover implementations for each tool
281
+ for tool_def in tools_metadata:
282
+ if isinstance(tool_def, dict) and tool_def.get("type") == "function":
283
+ func_def = tool_def.get("function", {})
284
+ func_name = func_def.get("name")
285
+
286
+ mapl_name = f"tool:{self._tools_name}/{func_name}"
287
+ if func_name and func_name not in self.user_tools:
288
+ if func_name in search_scope:
289
+ func = search_scope[func_name]
290
+ if callable(func):
291
+ logger.info(f"Auto-discovered tool: {func_name} -> {mapl_name}")
292
+ self._discovered_tools[func_name] = func
293
+ self.user_tools[func_name] = func
294
+ self.user_tools[mapl_name] = func
295
+ else:
296
+ logger.warning(f"Tool '{func_name}' not found in caller's scope")
297
+
298
+ # Sync all discovered tools with _tools_client (internal)
299
+ if self._discovered_tools:
300
+ for func_name, func in self._discovered_tools.items():
301
+ mapl_name = f"tool:{self._tools_name}/{func_name}"
302
+ def create_handler(tool_func):
303
+ def handler(params):
304
+ return tool_func(**params)
305
+ return handler
306
+ self._tools_client.register_tool(mapl_name, create_handler(func))
307
+ self._tools_registered = True
308
+ logger.info(f"Auto-registered {len(self._discovered_tools)} tools with _tools_client")
309
+
310
+ def _call_litellm(self, **params):
311
+ """
312
+ Call LiteLLM with configured api_base and api_key.
313
+ """
314
+ # Add api_base and api_key if configured
315
+ if self.api_base:
316
+ params['api_base'] = self.api_base
317
+ if self.api_key:
318
+ params['api_key'] = self.api_key
319
+
320
+ return litellm.completion(**params)
321
+
322
+ def _handle_generate(self, params: Dict[str, Any]):
323
+ """
324
+ Handle 'generate' tool (chat completions).
325
+
326
+ This runs on macaw_client. Tool callbacks route to _tools_client (internal).
327
+ Returns a serializable dict for MACAW protocol, or an iterator for streaming.
328
+ """
329
+ try:
330
+ # Check if streaming is requested
331
+ is_streaming = params.get('stream', False)
332
+
333
+ if is_streaming:
334
+ return self._stream_generate(params)
335
+
336
+ # Non-streaming mode
337
+ response = self._call_litellm(**params)
338
+
339
+ # Check if LLM wants to call tools
340
+ if hasattr(response.choices[0].message, 'tool_calls') and response.choices[0].message.tool_calls:
341
+ tool_calls = response.choices[0].message.tool_calls
342
+ logger.info(f"LLM requested {len(tool_calls)} tool calls")
343
+
344
+ # Process each tool call
345
+ tool_results = []
346
+ for tool_call in tool_calls:
347
+ func_name = tool_call.function.name
348
+ func_args = json.loads(tool_call.function.arguments)
349
+ mapl_name = f"tool:{self._tools_name}/{func_name}"
350
+
351
+ logger.info(f"Processing tool call: {func_name} -> {mapl_name}")
352
+
353
+ if func_name not in self.user_tools and mapl_name not in self.user_tools:
354
+ result = {
355
+ 'error': f"Tool not found: {mapl_name}",
356
+ 'available_tools': list(self.user_tools.keys())
357
+ }
358
+ else:
359
+ try:
360
+ result = self.macaw_client.invoke_tool(
361
+ tool_name=mapl_name,
362
+ parameters=func_args,
363
+ target_agent=self._tools_server_id
364
+ )
365
+ logger.info(f"Tool {func_name} executed successfully via _tools_client")
366
+ except Exception as e:
367
+ error_msg = str(e)
368
+ if 'denied' in error_msg.lower() or 'policy' in error_msg.lower():
369
+ logger.warning(f"MACAW blocked tool {func_name}: {error_msg}")
370
+ result = {
371
+ 'error': 'Access denied by security policy',
372
+ 'tool': func_name,
373
+ 'reason': 'Policy violation'
374
+ }
375
+ else:
376
+ result = {'error': str(e)}
377
+
378
+ tool_results.append({
379
+ 'tool_call_id': tool_call.id,
380
+ 'result': result
381
+ })
382
+
383
+ # Add tool results to conversation
384
+ messages = list(params.get('messages', []))
385
+
386
+ messages.append({
387
+ "role": "assistant",
388
+ "content": None,
389
+ "tool_calls": [{
390
+ "id": tc.id,
391
+ "type": "function",
392
+ "function": {
393
+ "name": tc.function.name,
394
+ "arguments": tc.function.arguments
395
+ }
396
+ } for tc in tool_calls]
397
+ })
398
+
399
+ for result in tool_results:
400
+ messages.append({
401
+ "role": "tool",
402
+ "tool_call_id": result['tool_call_id'],
403
+ "content": json.dumps(result['result'])
404
+ })
405
+
406
+ # Continue conversation with tool results
407
+ final_params = params.copy()
408
+ final_params['messages'] = messages
409
+
410
+ if 'tools' in final_params:
411
+ del final_params['tools']
412
+ if 'tool_choice' in final_params:
413
+ del final_params['tool_choice']
414
+
415
+ final_response = self._call_litellm(**final_params)
416
+ return final_response.model_dump()
417
+
418
+ return response.model_dump()
419
+
420
+ except Exception as e:
421
+ logger.error(f"Error in generate handler: {e}")
422
+ return {'error': str(e)}
423
+
424
+ def _stream_generate(self, params: Dict[str, Any]):
425
+ """
426
+ Handle streaming chat completions.
427
+ """
428
+ try:
429
+ # Add api_base and api_key if configured
430
+ if self.api_base:
431
+ params['api_base'] = self.api_base
432
+ if self.api_key:
433
+ params['api_key'] = self.api_key
434
+
435
+ stream = litellm.completion(**params)
436
+
437
+ for chunk in stream:
438
+ yield chunk.model_dump()
439
+
440
+ except Exception as e:
441
+ logger.error(f"Error in streaming generate: {e}")
442
+ yield {'error': str(e)}
443
+
444
+ def _handle_complete(self, params: Dict[str, Any]) -> Dict[str, Any]:
445
+ """
446
+ Handle 'complete' tool (text completions).
447
+ """
448
+ try:
449
+ if self.api_base:
450
+ params['api_base'] = self.api_base
451
+ if self.api_key:
452
+ params['api_key'] = self.api_key
453
+
454
+ response = litellm.text_completion(**params)
455
+ return response.model_dump()
456
+ except Exception as e:
457
+ logger.error(f"Error in complete handler: {e}")
458
+ return {'error': str(e)}
459
+
460
+ def _handle_embed(self, params: Dict[str, Any]) -> Dict[str, Any]:
461
+ """
462
+ Handle 'embed' tool (embeddings).
463
+ """
464
+ try:
465
+ if self.api_base:
466
+ params['api_base'] = self.api_base
467
+ if self.api_key:
468
+ params['api_key'] = self.api_key
469
+
470
+ response = litellm.embedding(**params)
471
+ return response.model_dump()
472
+ except Exception as e:
473
+ logger.error(f"Error in embed handler: {e}")
474
+ return {'error': str(e)}
475
+
476
+ # Namespace classes to mimic OpenAI API structure
477
+ class _ChatNamespace:
478
+ def __init__(self, parent):
479
+ self.parent = parent
480
+ self.completions = self._Completions(parent)
481
+
482
+ class _Completions:
483
+ def __init__(self, parent):
484
+ self.parent = parent
485
+
486
+ def create(self, **kwargs):
487
+ """
488
+ Create chat completion - routes through MACAW.
489
+ """
490
+ tools = kwargs.get('tools', kwargs.get('functions'))
491
+ if tools:
492
+ self.parent._auto_discover_tools(tools)
493
+
494
+ tool_name = f"tool:{self.parent.app_name}/generate"
495
+ is_streaming = kwargs.get('stream', False)
496
+
497
+ result = self.parent.macaw_client.invoke_tool(
498
+ tool_name=tool_name,
499
+ parameters=kwargs,
500
+ target_agent=self.parent.server_id,
501
+ stream=is_streaming
502
+ )
503
+
504
+ if is_streaming:
505
+ return self._wrap_streaming_response(result)
506
+
507
+ if isinstance(result, dict) and 'error' in result:
508
+ raise Exception(f"MACAW error: {result['error']}")
509
+
510
+ # Return as ModelResponse (LiteLLM's response type)
511
+ from litellm import ModelResponse
512
+ return ModelResponse(**result)
513
+
514
+ def _wrap_streaming_response(self, iterator):
515
+ """Wrap streaming chunks."""
516
+ for chunk in iterator:
517
+ if isinstance(chunk, dict):
518
+ if 'error' in chunk:
519
+ raise Exception(f"MACAW streaming error: {chunk['error']}")
520
+ yield chunk
521
+ else:
522
+ yield chunk
523
+
524
+ class _CompletionsNamespace:
525
+ def __init__(self, parent):
526
+ self.parent = parent
527
+
528
+ def create(self, **kwargs):
529
+ """Create text completion - routes through MACAW."""
530
+ tool_name = f"tool:{self.parent.app_name}/complete"
531
+
532
+ result = self.parent.macaw_client.invoke_tool(
533
+ tool_name=tool_name,
534
+ parameters=kwargs,
535
+ target_agent=self.parent.server_id
536
+ )
537
+
538
+ if isinstance(result, dict) and 'error' in result:
539
+ raise Exception(f"MACAW error: {result['error']}")
540
+
541
+ from litellm import TextCompletionResponse
542
+ return TextCompletionResponse(**result)
543
+
544
+ class _EmbeddingsNamespace:
545
+ def __init__(self, parent):
546
+ self.parent = parent
547
+
548
+ def create(self, **kwargs):
549
+ """Create embeddings - routes through MACAW."""
550
+ tool_name = f"tool:{self.parent.app_name}/embed"
551
+
552
+ result = self.parent.macaw_client.invoke_tool(
553
+ tool_name=tool_name,
554
+ parameters=kwargs,
555
+ target_agent=self.parent.server_id
556
+ )
557
+
558
+ if isinstance(result, dict) and 'error' in result:
559
+ raise Exception(f"MACAW error: {result['error']}")
560
+
561
+ from litellm import EmbeddingResponse
562
+ return EmbeddingResponse(**result)
563
+
564
+
565
+ class BoundSecureLiteLLM:
566
+ """
567
+ Per-user wrapper for SecureLiteLLM service.
568
+
569
+ Created via SecureLiteLLM.bind_to_user(user_client).
570
+ Routes all calls through the user's MACAWClient to the service's LLM client,
571
+ enabling per-user identity and policy enforcement.
572
+ """
573
+
574
+ def __init__(self, service: SecureLiteLLM, user_client: 'MACAWClient'):
575
+ self._service = service
576
+ self._user_client = user_client
577
+ self._bound = True
578
+
579
+ self.chat = self._ChatNamespace(self)
580
+ self.completions = self._CompletionsNamespace(self)
581
+ self.embeddings = self._EmbeddingsNamespace(self)
582
+
583
+ @property
584
+ def service(self) -> SecureLiteLLM:
585
+ if not self._bound:
586
+ raise RuntimeError("BoundSecureLiteLLM has been unbound.")
587
+ return self._service
588
+
589
+ @property
590
+ def user_client(self) -> 'MACAWClient':
591
+ if not self._bound:
592
+ raise RuntimeError("BoundSecureLiteLLM has been unbound.")
593
+ return self._user_client
594
+
595
+ def completion(self, model: str, messages: List[Dict], **kwargs):
596
+ """Direct completion call - convenience method."""
597
+ return self.chat.completions.create(model=model, messages=messages, **kwargs)
598
+
599
+ def unbind(self):
600
+ """Unbind this wrapper, invalidating all future calls."""
601
+ if not self._bound:
602
+ return
603
+ logger.info(f"Unbinding SecureLiteLLM from user {self._user_client.agent_id}")
604
+ self._bound = False
605
+ self._service = None
606
+ self._user_client = None
607
+
608
+ @property
609
+ def is_bound(self) -> bool:
610
+ return self._bound
611
+
612
+ class _ChatNamespace:
613
+ def __init__(self, bound: 'BoundSecureLiteLLM'):
614
+ self.bound = bound
615
+ self.completions = self._Completions(bound)
616
+
617
+ class _Completions:
618
+ def __init__(self, bound: 'BoundSecureLiteLLM'):
619
+ self.bound = bound
620
+
621
+ def create(self, **kwargs):
622
+ tool_name = f"tool:{self.bound.service.app_name}/generate"
623
+ is_streaming = kwargs.get('stream', False)
624
+
625
+ result = self.bound.user_client.invoke_tool(
626
+ tool_name=tool_name,
627
+ parameters=kwargs,
628
+ target_agent=self.bound.service.server_id,
629
+ stream=is_streaming
630
+ )
631
+
632
+ if is_streaming:
633
+ return self._wrap_streaming_response(result)
634
+
635
+ if isinstance(result, dict) and 'error' in result:
636
+ raise Exception(f"MACAW error: {result['error']}")
637
+
638
+ from litellm import ModelResponse
639
+ return ModelResponse(**result)
640
+
641
+ def _wrap_streaming_response(self, iterator):
642
+ for chunk in iterator:
643
+ if isinstance(chunk, dict):
644
+ if 'error' in chunk:
645
+ raise Exception(f"MACAW streaming error: {chunk['error']}")
646
+ yield chunk
647
+ else:
648
+ yield chunk
649
+
650
+ class _CompletionsNamespace:
651
+ def __init__(self, bound: 'BoundSecureLiteLLM'):
652
+ self.bound = bound
653
+
654
+ def create(self, **kwargs):
655
+ tool_name = f"tool:{self.bound.service.app_name}/complete"
656
+
657
+ result = self.bound.user_client.invoke_tool(
658
+ tool_name=tool_name,
659
+ parameters=kwargs,
660
+ target_agent=self.bound.service.server_id
661
+ )
662
+
663
+ if isinstance(result, dict) and 'error' in result:
664
+ raise Exception(f"MACAW error: {result['error']}")
665
+
666
+ from litellm import TextCompletionResponse
667
+ return TextCompletionResponse(**result)
668
+
669
+ class _EmbeddingsNamespace:
670
+ def __init__(self, bound: 'BoundSecureLiteLLM'):
671
+ self.bound = bound
672
+
673
+ def create(self, **kwargs):
674
+ tool_name = f"tool:{self.bound.service.app_name}/embed"
675
+
676
+ result = self.bound.user_client.invoke_tool(
677
+ tool_name=tool_name,
678
+ parameters=kwargs,
679
+ target_agent=self.bound.service.server_id
680
+ )
681
+
682
+ if isinstance(result, dict) and 'error' in result:
683
+ raise Exception(f"MACAW error: {result['error']}")
684
+
685
+ from litellm import EmbeddingResponse
686
+ return EmbeddingResponse(**result)