@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,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)
|