@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,876 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
SecureOpenAI - MACAW-protected OpenAI wrapper.
|
|
4
|
+
|
|
5
|
+
Drop-in replacement for OpenAI client with MACAW security.
|
|
6
|
+
Uses MAPL-compliant tool names: tool:<app_name>/generate, tool:<app_name>/complete, etc.
|
|
7
|
+
|
|
8
|
+
Supports three usage paths:
|
|
9
|
+
1. Direct on service: client.chat.completions.create() - simplest, app-level identity
|
|
10
|
+
2. bind_to_user: service.bind_to_user(user_client) - per-user identity for SaaS
|
|
11
|
+
3. invoke_tool: user.invoke_tool("tool:xxx/generate", ...) - explicit A2A control
|
|
12
|
+
|
|
13
|
+
Architecture (service mode):
|
|
14
|
+
- macaw_client: Primary client owning LLM operations (tool:{app_name}/generate, etc.)
|
|
15
|
+
- _tools_client: Internal client owning user-registered tools (tool:{app_name}-tools/*, not externally reachable)
|
|
16
|
+
- server_id: Points to macaw_client (the primary interface)
|
|
17
|
+
- Tool isolation: Only macaw_client can invoke tools on _tools_client
|
|
18
|
+
|
|
19
|
+
Install with: pip install -e /path/to/secureAI
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import os
|
|
23
|
+
import json
|
|
24
|
+
import logging
|
|
25
|
+
import inspect
|
|
26
|
+
from typing import Dict, Any, Optional, Callable, List
|
|
27
|
+
|
|
28
|
+
from openai import OpenAI
|
|
29
|
+
from macaw_client import MACAWClient
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
# MAPL-compliant resource naming: tool:<service>/<operation>
|
|
34
|
+
# Will be formatted with actual app_name at initialization
|
|
35
|
+
TOOL_OPERATIONS = {
|
|
36
|
+
"generate": "generate",
|
|
37
|
+
"complete": "complete",
|
|
38
|
+
"embed": "embed"
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class SecureOpenAI:
|
|
43
|
+
"""
|
|
44
|
+
Drop-in replacement for OpenAI client 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 OpenAI 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
|
+
api_key: str = None,
|
|
63
|
+
app_name: str = None,
|
|
64
|
+
intent_policy: Optional[Dict[str, Any]] = None,
|
|
65
|
+
# User mode parameters
|
|
66
|
+
jwt_token: str = None,
|
|
67
|
+
user_name: str = None
|
|
68
|
+
):
|
|
69
|
+
"""
|
|
70
|
+
Initialize SecureOpenAI wrapper.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
api_key: OpenAI API key (or from env)
|
|
74
|
+
app_name: Application name for registration
|
|
75
|
+
intent_policy: Application-defined MACAW intent policy
|
|
76
|
+
jwt_token: If provided, creates user agent with this identity (user mode)
|
|
77
|
+
user_name: Optional user name for user mode
|
|
78
|
+
"""
|
|
79
|
+
# Get API key
|
|
80
|
+
self.api_key = api_key or os.environ.get('OPENAI_API_KEY')
|
|
81
|
+
if not self.api_key:
|
|
82
|
+
raise ValueError("OpenAI API key required")
|
|
83
|
+
|
|
84
|
+
# Real OpenAI client
|
|
85
|
+
self.openai_client = OpenAI(api_key=self.api_key)
|
|
86
|
+
|
|
87
|
+
# Application identity
|
|
88
|
+
self.app_name = app_name or "secure-openai-app"
|
|
89
|
+
|
|
90
|
+
# Determine mode based on jwt_token
|
|
91
|
+
self._mode = "user" if jwt_token else "service"
|
|
92
|
+
self._jwt_token = jwt_token
|
|
93
|
+
self._user_name = user_name
|
|
94
|
+
|
|
95
|
+
# User-registered tools (OpenAI can call these)
|
|
96
|
+
self.user_tools = {}
|
|
97
|
+
|
|
98
|
+
# Auto-discovered tools cache
|
|
99
|
+
self._discovered_tools = {}
|
|
100
|
+
self._tools_registered = False
|
|
101
|
+
|
|
102
|
+
# Application-provided intent policy (no defaults!)
|
|
103
|
+
self.intent_policy = intent_policy or {}
|
|
104
|
+
|
|
105
|
+
if self._mode == "service":
|
|
106
|
+
# SERVICE MODE: Two-client architecture for tool isolation
|
|
107
|
+
# - macaw_client: Primary, owns LLM operations (externally reachable)
|
|
108
|
+
# - _tools_client: Internal, owns user tools (not externally reachable)
|
|
109
|
+
|
|
110
|
+
# Tools client name (for trace clarity)
|
|
111
|
+
self._tools_name = f"{self.app_name}-tools"
|
|
112
|
+
|
|
113
|
+
# LLM tools on the primary client
|
|
114
|
+
llm_tools = {
|
|
115
|
+
f"tool:{self.app_name}/generate": {
|
|
116
|
+
"handler": self._handle_generate,
|
|
117
|
+
"prompts": ["messages"]
|
|
118
|
+
},
|
|
119
|
+
f"tool:{self.app_name}/complete": {
|
|
120
|
+
"handler": self._handle_complete,
|
|
121
|
+
"prompts": ["prompt"]
|
|
122
|
+
},
|
|
123
|
+
f"tool:{self.app_name}/embed": {
|
|
124
|
+
"handler": self._handle_embed,
|
|
125
|
+
"prompts": ["input"]
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
# Primary client - owns LLM operations, externally reachable via server_id
|
|
130
|
+
self.macaw_client = MACAWClient(
|
|
131
|
+
app_name=self.app_name,
|
|
132
|
+
app_version="1.0.0",
|
|
133
|
+
intent_policy=self.intent_policy,
|
|
134
|
+
tools=llm_tools
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# Internal tools client - owns user-registered tools, NOT externally reachable
|
|
138
|
+
self._tools_client = MACAWClient(
|
|
139
|
+
app_name=self._tools_name,
|
|
140
|
+
app_version="1.0.0",
|
|
141
|
+
intent_policy=self.intent_policy,
|
|
142
|
+
tools={} # User tools added via register_tool
|
|
143
|
+
)
|
|
144
|
+
else:
|
|
145
|
+
# USER MODE: Create user agent with identity
|
|
146
|
+
self._tools_name = None
|
|
147
|
+
self._tools_client = None
|
|
148
|
+
|
|
149
|
+
self.macaw_client = MACAWClient(
|
|
150
|
+
user_name=user_name,
|
|
151
|
+
iam_token=jwt_token,
|
|
152
|
+
agent_type="user",
|
|
153
|
+
app_name=self.app_name,
|
|
154
|
+
app_version="1.0.0",
|
|
155
|
+
intent_policy=self.intent_policy or {
|
|
156
|
+
"purpose": f"OpenAI access for {user_name or 'user'}"
|
|
157
|
+
}
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# Register with LocalAgent and get server ID(s)
|
|
161
|
+
if self._mode == "service":
|
|
162
|
+
# Register both clients
|
|
163
|
+
if self.macaw_client.register() and self._tools_client.register():
|
|
164
|
+
self.server_id = self.macaw_client.agent_id # Primary interface!
|
|
165
|
+
self._tools_server_id = self._tools_client.agent_id # Internal, not exposed
|
|
166
|
+
logger.info(f"✅ SecureOpenAI registered (two-client mode):")
|
|
167
|
+
logger.info(f" Primary (LLM): {self.server_id}")
|
|
168
|
+
logger.info(f" Internal (tools): {self._tools_server_id}")
|
|
169
|
+
else:
|
|
170
|
+
raise RuntimeError("Failed to register with MACAW LocalAgent")
|
|
171
|
+
else:
|
|
172
|
+
# User mode - single client
|
|
173
|
+
if self.macaw_client.register():
|
|
174
|
+
self.server_id = self.macaw_client.agent_id
|
|
175
|
+
logger.info(f"✅ SecureOpenAI registered as: {self.server_id} (mode: {self._mode})")
|
|
176
|
+
else:
|
|
177
|
+
raise RuntimeError("Failed to register with MACAW LocalAgent")
|
|
178
|
+
|
|
179
|
+
# Mimic OpenAI API structure
|
|
180
|
+
# MACAW-protected namespaces (route through PEP)
|
|
181
|
+
self.chat = self._ChatNamespace(self)
|
|
182
|
+
self.completions = self._CompletionsNamespace(self)
|
|
183
|
+
self.embeddings = self._EmbeddingsNamespace(self)
|
|
184
|
+
|
|
185
|
+
# =========================================================================
|
|
186
|
+
# Pass-through properties for non-MACAW-protected APIs
|
|
187
|
+
# These delegate directly to the underlying OpenAI client
|
|
188
|
+
# =========================================================================
|
|
189
|
+
|
|
190
|
+
@property
|
|
191
|
+
def models(self):
|
|
192
|
+
"""
|
|
193
|
+
Pass-through to OpenAI models API.
|
|
194
|
+
|
|
195
|
+
Not MACAW-protected - provides direct access to model listing/retrieval.
|
|
196
|
+
"""
|
|
197
|
+
return self.openai_client.models
|
|
198
|
+
|
|
199
|
+
@property
|
|
200
|
+
def images(self):
|
|
201
|
+
"""
|
|
202
|
+
Pass-through to OpenAI images API (DALL-E).
|
|
203
|
+
|
|
204
|
+
Not MACAW-protected - image generation uses service's API key directly.
|
|
205
|
+
"""
|
|
206
|
+
return self.openai_client.images
|
|
207
|
+
|
|
208
|
+
@property
|
|
209
|
+
def audio(self):
|
|
210
|
+
"""
|
|
211
|
+
Pass-through to OpenAI audio API (Whisper/TTS).
|
|
212
|
+
|
|
213
|
+
Not MACAW-protected - audio transcription/speech uses service's API key directly.
|
|
214
|
+
"""
|
|
215
|
+
return self.openai_client.audio
|
|
216
|
+
|
|
217
|
+
@property
|
|
218
|
+
def files(self):
|
|
219
|
+
"""
|
|
220
|
+
Pass-through to OpenAI files API.
|
|
221
|
+
|
|
222
|
+
Not MACAW-protected - file operations use service's API key directly.
|
|
223
|
+
"""
|
|
224
|
+
return self.openai_client.files
|
|
225
|
+
|
|
226
|
+
@property
|
|
227
|
+
def fine_tuning(self):
|
|
228
|
+
"""
|
|
229
|
+
Pass-through to OpenAI fine-tuning API.
|
|
230
|
+
|
|
231
|
+
Not MACAW-protected - fine-tuning uses service's API key directly.
|
|
232
|
+
"""
|
|
233
|
+
return self.openai_client.fine_tuning
|
|
234
|
+
|
|
235
|
+
@property
|
|
236
|
+
def moderations(self):
|
|
237
|
+
"""
|
|
238
|
+
Pass-through to OpenAI moderations API.
|
|
239
|
+
|
|
240
|
+
Not MACAW-protected - content moderation uses service's API key directly.
|
|
241
|
+
"""
|
|
242
|
+
return self.openai_client.moderations
|
|
243
|
+
|
|
244
|
+
@property
|
|
245
|
+
def batches(self):
|
|
246
|
+
"""
|
|
247
|
+
Pass-through to OpenAI batches API.
|
|
248
|
+
|
|
249
|
+
Not MACAW-protected - batch operations use service's API key directly.
|
|
250
|
+
"""
|
|
251
|
+
return self.openai_client.batches
|
|
252
|
+
|
|
253
|
+
@property
|
|
254
|
+
def beta(self):
|
|
255
|
+
"""
|
|
256
|
+
Pass-through to OpenAI beta API (assistants, threads, vector stores).
|
|
257
|
+
|
|
258
|
+
Not MACAW-protected - beta APIs use service's API key directly.
|
|
259
|
+
"""
|
|
260
|
+
return self.openai_client.beta
|
|
261
|
+
|
|
262
|
+
def bind_to_user(self, user_client: 'MACAWClient') -> 'BoundSecureOpenAI':
|
|
263
|
+
"""
|
|
264
|
+
Bind this SecureOpenAI service to a user's MACAW client.
|
|
265
|
+
|
|
266
|
+
Creates a lightweight wrapper that routes calls through the user's
|
|
267
|
+
client to this service, enabling per-user identity and policy enforcement.
|
|
268
|
+
|
|
269
|
+
Only valid in service mode.
|
|
270
|
+
|
|
271
|
+
Security validations:
|
|
272
|
+
- Service must be in service mode
|
|
273
|
+
- User client must be a valid MACAWClient instance
|
|
274
|
+
- User client must be registered with LocalAgent
|
|
275
|
+
- User client should be a "user" type agent
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
user_client: A registered MACAWClient with user identity
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
BoundSecureOpenAI wrapper for this user
|
|
282
|
+
|
|
283
|
+
Raises:
|
|
284
|
+
ValueError: If service not in service mode or client validation fails
|
|
285
|
+
"""
|
|
286
|
+
if self._mode != "service":
|
|
287
|
+
raise ValueError("bind_to_user() only valid for service-mode SecureOpenAI")
|
|
288
|
+
|
|
289
|
+
# Validate user_client is a MACAWClient instance
|
|
290
|
+
if not hasattr(user_client, 'agent_id') or not hasattr(user_client, 'invoke_tool'):
|
|
291
|
+
raise ValueError("bind_to_user() requires a valid MACAWClient instance")
|
|
292
|
+
|
|
293
|
+
# Validate user_client is registered
|
|
294
|
+
if not getattr(user_client, 'registered', False):
|
|
295
|
+
raise ValueError("bind_to_user() requires a registered MACAWClient. Call register() first.")
|
|
296
|
+
|
|
297
|
+
# Validate agent_type is "user" (warning only, not blocking)
|
|
298
|
+
agent_type = getattr(user_client, 'agent_type', None)
|
|
299
|
+
if agent_type and agent_type != "user":
|
|
300
|
+
logger.warning(f"bind_to_user() called with agent_type='{agent_type}' (expected 'user'). "
|
|
301
|
+
f"User identity and policy enforcement may not work as expected.")
|
|
302
|
+
|
|
303
|
+
return BoundSecureOpenAI(self, user_client)
|
|
304
|
+
|
|
305
|
+
def register_tool(self, name: str, handler: Callable) -> 'SecureOpenAI':
|
|
306
|
+
"""
|
|
307
|
+
Register a tool that OpenAI can call.
|
|
308
|
+
|
|
309
|
+
Tools are registered on _tools_client (internal, not externally reachable).
|
|
310
|
+
This provides tool isolation - only the LLM can invoke these tools.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
name: Tool name (must match OpenAI function name)
|
|
314
|
+
handler: Function to handle tool execution
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
Self for chaining
|
|
318
|
+
"""
|
|
319
|
+
# Store with MAPL-compliant name using tools client's app_name
|
|
320
|
+
mapl_name = f"tool:{self._tools_name}/{name}"
|
|
321
|
+
|
|
322
|
+
# Wrap handler to accept params dict and unpack as kwargs
|
|
323
|
+
def wrapped_handler(params):
|
|
324
|
+
return handler(**params)
|
|
325
|
+
|
|
326
|
+
self.user_tools[name] = wrapped_handler # Store by simple name for lookup
|
|
327
|
+
self.user_tools[mapl_name] = wrapped_handler # Also store by MAPL name
|
|
328
|
+
|
|
329
|
+
# Register with _tools_client (internal) using MAPL name
|
|
330
|
+
self._tools_client.register_tool(mapl_name, wrapped_handler)
|
|
331
|
+
|
|
332
|
+
logger.info(f"Registered tool: {name} -> {mapl_name}")
|
|
333
|
+
return self
|
|
334
|
+
|
|
335
|
+
def _auto_discover_tools(self, tools_metadata):
|
|
336
|
+
"""
|
|
337
|
+
Auto-discover tool implementations from caller's context.
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
tools_metadata: List of tool definitions from OpenAI call
|
|
341
|
+
"""
|
|
342
|
+
if self._tools_registered:
|
|
343
|
+
return # Already discovered
|
|
344
|
+
|
|
345
|
+
# Get caller's frame (need to go back through the call stack)
|
|
346
|
+
frame = inspect.currentframe()
|
|
347
|
+
caller_frame = frame.f_back.f_back.f_back
|
|
348
|
+
if not caller_frame:
|
|
349
|
+
logger.warning("Could not access caller frame for auto-discovery")
|
|
350
|
+
return
|
|
351
|
+
|
|
352
|
+
caller_globals = caller_frame.f_globals
|
|
353
|
+
caller_locals = caller_frame.f_locals
|
|
354
|
+
|
|
355
|
+
# Combine locals and globals for search
|
|
356
|
+
search_scope = {**caller_globals, **caller_locals}
|
|
357
|
+
|
|
358
|
+
# Discover implementations for each tool
|
|
359
|
+
for tool_def in tools_metadata:
|
|
360
|
+
if isinstance(tool_def, dict) and tool_def.get("type") == "function":
|
|
361
|
+
func_def = tool_def.get("function", {})
|
|
362
|
+
func_name = func_def.get("name")
|
|
363
|
+
|
|
364
|
+
# Use MAPL-compliant name with tools client: tool:<app_name>-tools/<func_name>
|
|
365
|
+
mapl_name = f"tool:{self._tools_name}/{func_name}"
|
|
366
|
+
if func_name and func_name not in self.user_tools:
|
|
367
|
+
# Look for function in caller's scope
|
|
368
|
+
if func_name in search_scope:
|
|
369
|
+
func = search_scope[func_name]
|
|
370
|
+
if callable(func):
|
|
371
|
+
logger.info(f"Auto-discovered tool: {func_name} -> {mapl_name}")
|
|
372
|
+
self._discovered_tools[func_name] = func
|
|
373
|
+
self.user_tools[func_name] = func
|
|
374
|
+
self.user_tools[mapl_name] = func
|
|
375
|
+
else:
|
|
376
|
+
logger.warning(f"Tool '{func_name}' not found in caller's scope")
|
|
377
|
+
|
|
378
|
+
# Sync all discovered tools with _tools_client (internal)
|
|
379
|
+
if self._discovered_tools:
|
|
380
|
+
# Register each discovered tool with _tools_client
|
|
381
|
+
for func_name, func in self._discovered_tools.items():
|
|
382
|
+
mapl_name = f"tool:{self._tools_name}/{func_name}"
|
|
383
|
+
# Create wrapper handler that accepts dict parameter
|
|
384
|
+
def create_handler(tool_func):
|
|
385
|
+
def handler(params):
|
|
386
|
+
return tool_func(**params)
|
|
387
|
+
return handler
|
|
388
|
+
self._tools_client.register_tool(mapl_name, create_handler(func))
|
|
389
|
+
self._tools_registered = True
|
|
390
|
+
logger.info(f"Auto-registered {len(self._discovered_tools)} tools with _tools_client")
|
|
391
|
+
|
|
392
|
+
def _handle_generate(self, params: Dict[str, Any]):
|
|
393
|
+
"""
|
|
394
|
+
Handle 'generate' tool (chat completions).
|
|
395
|
+
|
|
396
|
+
This runs on macaw_client. Tool callbacks route to _tools_client (internal).
|
|
397
|
+
Returns a serializable dict for MACAW protocol, or an iterator for streaming.
|
|
398
|
+
"""
|
|
399
|
+
try:
|
|
400
|
+
# Check if streaming is requested
|
|
401
|
+
is_streaming = params.get('stream', False)
|
|
402
|
+
|
|
403
|
+
if is_streaming:
|
|
404
|
+
# Streaming mode: return iterator
|
|
405
|
+
return self._stream_generate(params)
|
|
406
|
+
|
|
407
|
+
# Non-streaming mode: existing behavior
|
|
408
|
+
# Call real OpenAI (params are clean - no MACAW internals)
|
|
409
|
+
response = self.openai_client.chat.completions.create(**params)
|
|
410
|
+
|
|
411
|
+
# Check if OpenAI wants to call tools
|
|
412
|
+
if response.choices[0].message.tool_calls:
|
|
413
|
+
tool_calls = response.choices[0].message.tool_calls
|
|
414
|
+
logger.info(f"OpenAI requested {len(tool_calls)} tool calls")
|
|
415
|
+
|
|
416
|
+
# Process each tool call
|
|
417
|
+
tool_results = []
|
|
418
|
+
for tool_call in tool_calls:
|
|
419
|
+
func_name = tool_call.function.name
|
|
420
|
+
func_args = json.loads(tool_call.function.arguments)
|
|
421
|
+
# Convert to MAPL-compliant name using tools client: tool:<app_name>-tools/<func_name>
|
|
422
|
+
mapl_name = f"tool:{self._tools_name}/{func_name}"
|
|
423
|
+
|
|
424
|
+
logger.info(f"Processing tool call: {func_name} -> {mapl_name}")
|
|
425
|
+
|
|
426
|
+
# Check if we have this tool (by simple name or MAPL name)
|
|
427
|
+
if func_name not in self.user_tools and mapl_name not in self.user_tools:
|
|
428
|
+
result = {
|
|
429
|
+
'error': f"Tool not found: {mapl_name}",
|
|
430
|
+
'available_tools': list(self.user_tools.keys())
|
|
431
|
+
}
|
|
432
|
+
else:
|
|
433
|
+
# Invoke tool through MACAW (goes through PEP!)
|
|
434
|
+
# Route from macaw_client to _tools_client (internal)
|
|
435
|
+
# This provides tool isolation: only LLM can call tools
|
|
436
|
+
try:
|
|
437
|
+
result = self.macaw_client.invoke_tool(
|
|
438
|
+
tool_name=mapl_name,
|
|
439
|
+
parameters=func_args,
|
|
440
|
+
target_agent=self._tools_server_id # Internal tools client
|
|
441
|
+
)
|
|
442
|
+
logger.info(f"Tool {func_name} executed successfully via _tools_client")
|
|
443
|
+
except Exception as e:
|
|
444
|
+
error_msg = str(e)
|
|
445
|
+
# Check if MACAW blocked it
|
|
446
|
+
if 'denied' in error_msg.lower() or 'policy' in error_msg.lower():
|
|
447
|
+
logger.warning(f"🛡️ MACAW blocked tool {func_name}: {error_msg}")
|
|
448
|
+
result = {
|
|
449
|
+
'error': 'Access denied by security policy',
|
|
450
|
+
'tool': func_name,
|
|
451
|
+
'reason': 'Policy violation'
|
|
452
|
+
}
|
|
453
|
+
else:
|
|
454
|
+
result = {'error': str(e)}
|
|
455
|
+
|
|
456
|
+
tool_results.append({
|
|
457
|
+
'tool_call_id': tool_call.id,
|
|
458
|
+
'result': result
|
|
459
|
+
})
|
|
460
|
+
|
|
461
|
+
# Add tool results to conversation
|
|
462
|
+
messages = list(params.get('messages', []))
|
|
463
|
+
|
|
464
|
+
# Add assistant message with tool calls
|
|
465
|
+
messages.append({
|
|
466
|
+
"role": "assistant",
|
|
467
|
+
"content": None,
|
|
468
|
+
"tool_calls": [{
|
|
469
|
+
"id": tc.id,
|
|
470
|
+
"type": "function",
|
|
471
|
+
"function": {
|
|
472
|
+
"name": tc.function.name,
|
|
473
|
+
"arguments": tc.function.arguments
|
|
474
|
+
}
|
|
475
|
+
} for tc in tool_calls]
|
|
476
|
+
})
|
|
477
|
+
|
|
478
|
+
# Add tool results
|
|
479
|
+
for result in tool_results:
|
|
480
|
+
messages.append({
|
|
481
|
+
"role": "tool",
|
|
482
|
+
"tool_call_id": result['tool_call_id'],
|
|
483
|
+
"content": json.dumps(result['result'])
|
|
484
|
+
})
|
|
485
|
+
|
|
486
|
+
# Continue conversation with tool results
|
|
487
|
+
final_params = params.copy()
|
|
488
|
+
final_params['messages'] = messages
|
|
489
|
+
|
|
490
|
+
# Remove tools and tool_choice to prevent infinite recursion
|
|
491
|
+
if 'tools' in final_params:
|
|
492
|
+
del final_params['tools']
|
|
493
|
+
if 'tool_choice' in final_params:
|
|
494
|
+
del final_params['tool_choice']
|
|
495
|
+
|
|
496
|
+
final_response = self.openai_client.chat.completions.create(**final_params)
|
|
497
|
+
|
|
498
|
+
# Convert to serializable dict for MACAW protocol
|
|
499
|
+
return final_response.model_dump()
|
|
500
|
+
|
|
501
|
+
# No tool calls, convert response to dict
|
|
502
|
+
return response.model_dump()
|
|
503
|
+
|
|
504
|
+
except Exception as e:
|
|
505
|
+
logger.error(f"Error in generate handler: {e}")
|
|
506
|
+
return {'error': str(e)}
|
|
507
|
+
|
|
508
|
+
def _stream_generate(self, params: Dict[str, Any]):
|
|
509
|
+
"""
|
|
510
|
+
Handle streaming chat completions.
|
|
511
|
+
|
|
512
|
+
Returns an iterator that yields chunks from the OpenAI streaming API.
|
|
513
|
+
Each chunk is converted to a serializable dict.
|
|
514
|
+
"""
|
|
515
|
+
try:
|
|
516
|
+
# Call OpenAI with streaming
|
|
517
|
+
stream = self.openai_client.chat.completions.create(**params)
|
|
518
|
+
|
|
519
|
+
# Yield each chunk as a serializable dict
|
|
520
|
+
for chunk in stream:
|
|
521
|
+
yield chunk.model_dump()
|
|
522
|
+
|
|
523
|
+
except Exception as e:
|
|
524
|
+
logger.error(f"Error in streaming generate: {e}")
|
|
525
|
+
# Yield error as final chunk
|
|
526
|
+
yield {'error': str(e)}
|
|
527
|
+
|
|
528
|
+
def _handle_complete(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
529
|
+
"""
|
|
530
|
+
Handle 'complete' tool (text completions).
|
|
531
|
+
|
|
532
|
+
Returns a serializable dict for MACAW protocol.
|
|
533
|
+
"""
|
|
534
|
+
try:
|
|
535
|
+
response = self.openai_client.completions.create(**params)
|
|
536
|
+
return response.model_dump()
|
|
537
|
+
except Exception as e:
|
|
538
|
+
logger.error(f"Error in complete handler: {e}")
|
|
539
|
+
return {'error': str(e)}
|
|
540
|
+
|
|
541
|
+
def _handle_embed(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
542
|
+
"""
|
|
543
|
+
Handle 'embed' tool (embeddings).
|
|
544
|
+
|
|
545
|
+
Returns a serializable dict for MACAW protocol.
|
|
546
|
+
"""
|
|
547
|
+
try:
|
|
548
|
+
response = self.openai_client.embeddings.create(**params)
|
|
549
|
+
return response.model_dump()
|
|
550
|
+
except Exception as e:
|
|
551
|
+
logger.error(f"Error in embed handler: {e}")
|
|
552
|
+
return {'error': str(e)}
|
|
553
|
+
|
|
554
|
+
# Namespace classes to mimic OpenAI API structure
|
|
555
|
+
class _ChatNamespace:
|
|
556
|
+
def __init__(self, parent):
|
|
557
|
+
self.parent = parent
|
|
558
|
+
self.completions = self._Completions(parent)
|
|
559
|
+
|
|
560
|
+
class _Completions:
|
|
561
|
+
def __init__(self, parent):
|
|
562
|
+
self.parent = parent
|
|
563
|
+
|
|
564
|
+
def create(self, **kwargs):
|
|
565
|
+
"""
|
|
566
|
+
Create chat completion - routes through MACAW.
|
|
567
|
+
|
|
568
|
+
This mimics OpenAI's API but goes through PEP!
|
|
569
|
+
Routes to macaw_client (the primary interface).
|
|
570
|
+
Supports streaming when stream=True is passed.
|
|
571
|
+
"""
|
|
572
|
+
# Auto-discover tools if provided
|
|
573
|
+
tools = kwargs.get('tools', kwargs.get('functions'))
|
|
574
|
+
if tools:
|
|
575
|
+
self.parent._auto_discover_tools(tools)
|
|
576
|
+
|
|
577
|
+
# Route to primary client (macaw_client)
|
|
578
|
+
tool_name = f"tool:{self.parent.app_name}/generate"
|
|
579
|
+
is_streaming = kwargs.get('stream', False)
|
|
580
|
+
|
|
581
|
+
# Route through macaw_client (the primary LLM service)
|
|
582
|
+
result = self.parent.macaw_client.invoke_tool(
|
|
583
|
+
tool_name=tool_name,
|
|
584
|
+
parameters=kwargs,
|
|
585
|
+
target_agent=self.parent.server_id, # = macaw_client.agent_id
|
|
586
|
+
stream=is_streaming
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
if is_streaming:
|
|
590
|
+
# Streaming mode: result is a StreamingResultIterator, wrap in OpenAI types
|
|
591
|
+
return self._wrap_streaming_response(result)
|
|
592
|
+
|
|
593
|
+
# Non-streaming mode: handle errors and reconstruct type
|
|
594
|
+
if isinstance(result, dict) and 'error' in result:
|
|
595
|
+
raise Exception(f"MACAW error: {result['error']}")
|
|
596
|
+
|
|
597
|
+
# Reconstruct ChatCompletion from dict
|
|
598
|
+
from openai.types.chat import ChatCompletion
|
|
599
|
+
return ChatCompletion(**result)
|
|
600
|
+
|
|
601
|
+
def _wrap_streaming_response(self, iterator):
|
|
602
|
+
"""Wrap streaming chunks in OpenAI ChatCompletionChunk objects."""
|
|
603
|
+
from openai.types.chat import ChatCompletionChunk
|
|
604
|
+
|
|
605
|
+
for chunk in iterator:
|
|
606
|
+
if isinstance(chunk, dict):
|
|
607
|
+
if 'error' in chunk:
|
|
608
|
+
raise Exception(f"MACAW streaming error: {chunk['error']}")
|
|
609
|
+
# Convert dict to ChatCompletionChunk
|
|
610
|
+
try:
|
|
611
|
+
yield ChatCompletionChunk(**chunk)
|
|
612
|
+
except Exception as e:
|
|
613
|
+
logger.warning(f"Could not convert chunk to ChatCompletionChunk: {e}")
|
|
614
|
+
yield chunk # Return raw dict if conversion fails
|
|
615
|
+
else:
|
|
616
|
+
yield chunk
|
|
617
|
+
|
|
618
|
+
class _CompletionsNamespace:
|
|
619
|
+
def __init__(self, parent):
|
|
620
|
+
self.parent = parent
|
|
621
|
+
|
|
622
|
+
def create(self, **kwargs):
|
|
623
|
+
"""Create text completion - routes through MACAW."""
|
|
624
|
+
tool_name = f"tool:{self.parent.app_name}/complete"
|
|
625
|
+
|
|
626
|
+
# Route through macaw_client
|
|
627
|
+
result = self.parent.macaw_client.invoke_tool(
|
|
628
|
+
tool_name=tool_name,
|
|
629
|
+
parameters=kwargs,
|
|
630
|
+
target_agent=self.parent.server_id # = macaw_client.agent_id
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
# Handle errors
|
|
634
|
+
if isinstance(result, dict) and 'error' in result:
|
|
635
|
+
raise Exception(f"MACAW error: {result['error']}")
|
|
636
|
+
|
|
637
|
+
# Reconstruct Completion from dict
|
|
638
|
+
from openai.types import Completion
|
|
639
|
+
return Completion(**result)
|
|
640
|
+
|
|
641
|
+
class _EmbeddingsNamespace:
|
|
642
|
+
def __init__(self, parent):
|
|
643
|
+
self.parent = parent
|
|
644
|
+
|
|
645
|
+
def create(self, **kwargs):
|
|
646
|
+
"""Create embeddings - routes through MACAW."""
|
|
647
|
+
tool_name = f"tool:{self.parent.app_name}/embed"
|
|
648
|
+
|
|
649
|
+
# Route through macaw_client
|
|
650
|
+
result = self.parent.macaw_client.invoke_tool(
|
|
651
|
+
tool_name=tool_name,
|
|
652
|
+
parameters=kwargs,
|
|
653
|
+
target_agent=self.parent.server_id # = macaw_client.agent_id
|
|
654
|
+
)
|
|
655
|
+
|
|
656
|
+
# Handle errors
|
|
657
|
+
if isinstance(result, dict) and 'error' in result:
|
|
658
|
+
raise Exception(f"MACAW error: {result['error']}")
|
|
659
|
+
|
|
660
|
+
# Reconstruct CreateEmbeddingResponse from dict
|
|
661
|
+
from openai.types import CreateEmbeddingResponse
|
|
662
|
+
return CreateEmbeddingResponse(**result)
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
class BoundSecureOpenAI:
|
|
666
|
+
"""
|
|
667
|
+
Per-user wrapper for SecureOpenAI service.
|
|
668
|
+
|
|
669
|
+
Created via SecureOpenAI.bind_to_user(user_client).
|
|
670
|
+
Routes all calls through the user's MACAWClient to the service's LLM client,
|
|
671
|
+
enabling per-user identity and policy enforcement.
|
|
672
|
+
|
|
673
|
+
Authenticated prompts are auto-created by invoke_tool based on
|
|
674
|
+
the service's prompts declaration in the registry.
|
|
675
|
+
|
|
676
|
+
Call unbind() to invalidate this binding when done.
|
|
677
|
+
"""
|
|
678
|
+
|
|
679
|
+
def __init__(self, service: SecureOpenAI, user_client: 'MACAWClient'):
|
|
680
|
+
"""
|
|
681
|
+
Initialize bound wrapper.
|
|
682
|
+
|
|
683
|
+
Args:
|
|
684
|
+
service: The shared SecureOpenAI service (must be in service mode)
|
|
685
|
+
user_client: User's registered MACAWClient with identity
|
|
686
|
+
"""
|
|
687
|
+
self._service = service
|
|
688
|
+
self._user_client = user_client
|
|
689
|
+
self._bound = True
|
|
690
|
+
|
|
691
|
+
# Create OpenAI-compatible API namespaces
|
|
692
|
+
self.chat = self._ChatNamespace(self)
|
|
693
|
+
self.completions = self._CompletionsNamespace(self)
|
|
694
|
+
self.embeddings = self._EmbeddingsNamespace(self)
|
|
695
|
+
|
|
696
|
+
@property
|
|
697
|
+
def service(self) -> SecureOpenAI:
|
|
698
|
+
"""Get the bound service (raises if unbound)."""
|
|
699
|
+
if not self._bound:
|
|
700
|
+
raise RuntimeError("BoundSecureOpenAI has been unbound. Create a new binding with bind_to_user().")
|
|
701
|
+
return self._service
|
|
702
|
+
|
|
703
|
+
@property
|
|
704
|
+
def user_client(self) -> 'MACAWClient':
|
|
705
|
+
"""Get the bound user client (raises if unbound)."""
|
|
706
|
+
if not self._bound:
|
|
707
|
+
raise RuntimeError("BoundSecureOpenAI has been unbound. Create a new binding with bind_to_user().")
|
|
708
|
+
return self._user_client
|
|
709
|
+
|
|
710
|
+
def unbind(self):
|
|
711
|
+
"""
|
|
712
|
+
Unbind this wrapper, invalidating all future calls.
|
|
713
|
+
|
|
714
|
+
After unbinding:
|
|
715
|
+
- All API calls will raise RuntimeError
|
|
716
|
+
- References to service and user_client are cleared
|
|
717
|
+
- A new binding must be created via bind_to_user()
|
|
718
|
+
|
|
719
|
+
This is useful for cleanup and ensuring bindings are not reused
|
|
720
|
+
after a user session ends.
|
|
721
|
+
"""
|
|
722
|
+
if not self._bound:
|
|
723
|
+
logger.warning("BoundSecureOpenAI.unbind() called on already unbound instance")
|
|
724
|
+
return
|
|
725
|
+
|
|
726
|
+
logger.info(f"Unbinding SecureOpenAI from user {self._user_client.agent_id}")
|
|
727
|
+
self._bound = False
|
|
728
|
+
self._service = None
|
|
729
|
+
self._user_client = None
|
|
730
|
+
|
|
731
|
+
@property
|
|
732
|
+
def is_bound(self) -> bool:
|
|
733
|
+
"""Check if this wrapper is still bound."""
|
|
734
|
+
return self._bound
|
|
735
|
+
|
|
736
|
+
# =========================================================================
|
|
737
|
+
# Pass-through properties for non-MACAW-protected APIs
|
|
738
|
+
# These delegate to the service's underlying OpenAI client
|
|
739
|
+
# =========================================================================
|
|
740
|
+
|
|
741
|
+
@property
|
|
742
|
+
def models(self):
|
|
743
|
+
"""Pass-through to OpenAI models API via service."""
|
|
744
|
+
return self.service.openai_client.models
|
|
745
|
+
|
|
746
|
+
@property
|
|
747
|
+
def images(self):
|
|
748
|
+
"""Pass-through to OpenAI images API (DALL-E) via service."""
|
|
749
|
+
return self.service.openai_client.images
|
|
750
|
+
|
|
751
|
+
@property
|
|
752
|
+
def audio(self):
|
|
753
|
+
"""Pass-through to OpenAI audio API (Whisper/TTS) via service."""
|
|
754
|
+
return self.service.openai_client.audio
|
|
755
|
+
|
|
756
|
+
@property
|
|
757
|
+
def files(self):
|
|
758
|
+
"""Pass-through to OpenAI files API via service."""
|
|
759
|
+
return self.service.openai_client.files
|
|
760
|
+
|
|
761
|
+
@property
|
|
762
|
+
def fine_tuning(self):
|
|
763
|
+
"""Pass-through to OpenAI fine-tuning API via service."""
|
|
764
|
+
return self.service.openai_client.fine_tuning
|
|
765
|
+
|
|
766
|
+
@property
|
|
767
|
+
def moderations(self):
|
|
768
|
+
"""Pass-through to OpenAI moderations API via service."""
|
|
769
|
+
return self.service.openai_client.moderations
|
|
770
|
+
|
|
771
|
+
@property
|
|
772
|
+
def batches(self):
|
|
773
|
+
"""Pass-through to OpenAI batches API via service."""
|
|
774
|
+
return self.service.openai_client.batches
|
|
775
|
+
|
|
776
|
+
@property
|
|
777
|
+
def beta(self):
|
|
778
|
+
"""Pass-through to OpenAI beta API (assistants, threads, vector stores) via service."""
|
|
779
|
+
return self.service.openai_client.beta
|
|
780
|
+
|
|
781
|
+
class _ChatNamespace:
|
|
782
|
+
def __init__(self, bound: 'BoundSecureOpenAI'):
|
|
783
|
+
self.bound = bound
|
|
784
|
+
self.completions = self._Completions(bound)
|
|
785
|
+
|
|
786
|
+
class _Completions:
|
|
787
|
+
def __init__(self, bound: 'BoundSecureOpenAI'):
|
|
788
|
+
self.bound = bound
|
|
789
|
+
|
|
790
|
+
def create(self, **kwargs):
|
|
791
|
+
"""
|
|
792
|
+
Create chat completion via user's client → LLM service.
|
|
793
|
+
|
|
794
|
+
Routes to server_id (which is _llm_client.agent_id).
|
|
795
|
+
Authenticated prompts are auto-created by invoke_tool.
|
|
796
|
+
Supports streaming with stream=True parameter.
|
|
797
|
+
"""
|
|
798
|
+
tool_name = f"tool:{self.bound.service.app_name}/generate"
|
|
799
|
+
is_streaming = kwargs.get('stream', False)
|
|
800
|
+
|
|
801
|
+
# Route through user's client to LLM service
|
|
802
|
+
result = self.bound.user_client.invoke_tool(
|
|
803
|
+
tool_name=tool_name,
|
|
804
|
+
parameters=kwargs,
|
|
805
|
+
target_agent=self.bound.service.server_id, # = _llm_client.agent_id
|
|
806
|
+
stream=is_streaming
|
|
807
|
+
)
|
|
808
|
+
|
|
809
|
+
if is_streaming:
|
|
810
|
+
# Streaming mode: result is a StreamingResultIterator, wrap in OpenAI types
|
|
811
|
+
return self._wrap_streaming_response(result)
|
|
812
|
+
|
|
813
|
+
# Handle errors
|
|
814
|
+
if isinstance(result, dict) and 'error' in result:
|
|
815
|
+
raise Exception(f"MACAW error: {result['error']}")
|
|
816
|
+
|
|
817
|
+
# Reconstruct ChatCompletion from dict
|
|
818
|
+
from openai.types.chat import ChatCompletion
|
|
819
|
+
return ChatCompletion(**result)
|
|
820
|
+
|
|
821
|
+
def _wrap_streaming_response(self, iterator):
|
|
822
|
+
"""Wrap streaming chunks in OpenAI ChatCompletionChunk objects."""
|
|
823
|
+
from openai.types.chat import ChatCompletionChunk
|
|
824
|
+
|
|
825
|
+
for chunk in iterator:
|
|
826
|
+
if isinstance(chunk, dict):
|
|
827
|
+
if 'error' in chunk:
|
|
828
|
+
raise Exception(f"MACAW streaming error: {chunk['error']}")
|
|
829
|
+
# Convert dict to ChatCompletionChunk
|
|
830
|
+
try:
|
|
831
|
+
yield ChatCompletionChunk(**chunk)
|
|
832
|
+
except Exception as e:
|
|
833
|
+
logger.warning(f"Could not convert chunk to ChatCompletionChunk: {e}")
|
|
834
|
+
yield chunk # Return raw dict if conversion fails
|
|
835
|
+
else:
|
|
836
|
+
yield chunk
|
|
837
|
+
|
|
838
|
+
class _CompletionsNamespace:
|
|
839
|
+
def __init__(self, bound: 'BoundSecureOpenAI'):
|
|
840
|
+
self.bound = bound
|
|
841
|
+
|
|
842
|
+
def create(self, **kwargs):
|
|
843
|
+
"""Create text completion via user's client → LLM service."""
|
|
844
|
+
tool_name = f"tool:{self.bound.service.app_name}/complete"
|
|
845
|
+
|
|
846
|
+
result = self.bound.user_client.invoke_tool(
|
|
847
|
+
tool_name=tool_name,
|
|
848
|
+
parameters=kwargs,
|
|
849
|
+
target_agent=self.bound.service.server_id # = _llm_client.agent_id
|
|
850
|
+
)
|
|
851
|
+
|
|
852
|
+
if isinstance(result, dict) and 'error' in result:
|
|
853
|
+
raise Exception(f"MACAW error: {result['error']}")
|
|
854
|
+
|
|
855
|
+
from openai.types import Completion
|
|
856
|
+
return Completion(**result)
|
|
857
|
+
|
|
858
|
+
class _EmbeddingsNamespace:
|
|
859
|
+
def __init__(self, bound: 'BoundSecureOpenAI'):
|
|
860
|
+
self.bound = bound
|
|
861
|
+
|
|
862
|
+
def create(self, **kwargs):
|
|
863
|
+
"""Create embeddings via user's client → LLM service."""
|
|
864
|
+
tool_name = f"tool:{self.bound.service.app_name}/embed"
|
|
865
|
+
|
|
866
|
+
result = self.bound.user_client.invoke_tool(
|
|
867
|
+
tool_name=tool_name,
|
|
868
|
+
parameters=kwargs,
|
|
869
|
+
target_agent=self.bound.service.server_id # = _llm_client.agent_id
|
|
870
|
+
)
|
|
871
|
+
|
|
872
|
+
if isinstance(result, dict) and 'error' in result:
|
|
873
|
+
raise Exception(f"MACAW error: {result['error']}")
|
|
874
|
+
|
|
875
|
+
from openai.types import CreateEmbeddingResponse
|
|
876
|
+
return CreateEmbeddingResponse(**result)
|