blaxel 0.64.0__py3-none-any.whl
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.
- blaxel/__init__.py +8 -0
- blaxel/agents/__init__.py +5 -0
- blaxel/agents/chain.py +153 -0
- blaxel/agents/chat.py +286 -0
- blaxel/agents/decorator.py +208 -0
- blaxel/agents/thread.py +24 -0
- blaxel/agents/voice/openai.py +255 -0
- blaxel/agents/voice/utils.py +25 -0
- blaxel/api/__init__.py +1 -0
- blaxel/api/agents/__init__.py +0 -0
- blaxel/api/agents/create_agent.py +155 -0
- blaxel/api/agents/delete_agent.py +146 -0
- blaxel/api/agents/get_agent.py +146 -0
- blaxel/api/agents/get_agent_logs.py +151 -0
- blaxel/api/agents/get_agent_metrics.py +150 -0
- blaxel/api/agents/get_agent_trace_ids.py +201 -0
- blaxel/api/agents/list_agent_revisions.py +155 -0
- blaxel/api/agents/list_agents.py +127 -0
- blaxel/api/agents/update_agent.py +168 -0
- blaxel/api/configurations/__init__.py +0 -0
- blaxel/api/configurations/get_configuration.py +122 -0
- blaxel/api/default/__init__.py +0 -0
- blaxel/api/default/get_trace.py +150 -0
- blaxel/api/default/get_trace_ids.py +218 -0
- blaxel/api/default/get_trace_logs.py +186 -0
- blaxel/api/default/list_mcp_hub_definitions.py +127 -0
- blaxel/api/functions/__init__.py +0 -0
- blaxel/api/functions/create_function.py +155 -0
- blaxel/api/functions/delete_function.py +146 -0
- blaxel/api/functions/get_function.py +146 -0
- blaxel/api/functions/get_function_logs.py +151 -0
- blaxel/api/functions/get_function_metrics.py +150 -0
- blaxel/api/functions/get_function_trace_ids.py +201 -0
- blaxel/api/functions/list_function_revisions.py +158 -0
- blaxel/api/functions/list_functions.py +131 -0
- blaxel/api/functions/update_function.py +168 -0
- blaxel/api/integrations/__init__.py +0 -0
- blaxel/api/integrations/create_integration_connection.py +167 -0
- blaxel/api/integrations/delete_integration_connection.py +158 -0
- blaxel/api/integrations/get_integration.py +97 -0
- blaxel/api/integrations/get_integration_connection.py +158 -0
- blaxel/api/integrations/get_integration_connection_model.py +104 -0
- blaxel/api/integrations/get_integration_connection_model_endpoint_configurations.py +97 -0
- blaxel/api/integrations/list_integration_connection_models.py +97 -0
- blaxel/api/integrations/list_integration_connections.py +139 -0
- blaxel/api/integrations/update_integration_connection.py +180 -0
- blaxel/api/invitations/__init__.py +0 -0
- blaxel/api/invitations/list_all_pending_invitations.py +142 -0
- blaxel/api/knowledgebases/__init__.py +0 -0
- blaxel/api/knowledgebases/create_knowledgebase.py +163 -0
- blaxel/api/knowledgebases/delete_knowledgebase.py +154 -0
- blaxel/api/knowledgebases/get_knowledgebase.py +154 -0
- blaxel/api/knowledgebases/list_knowledgebase_revisions.py +158 -0
- blaxel/api/knowledgebases/list_knowledgebases.py +139 -0
- blaxel/api/knowledgebases/update_knowledgebase.py +176 -0
- blaxel/api/locations/__init__.py +0 -0
- blaxel/api/locations/list_locations.py +139 -0
- blaxel/api/metrics/__init__.py +0 -0
- blaxel/api/metrics/get_metrics.py +130 -0
- blaxel/api/models/__init__.py +0 -0
- blaxel/api/models/create_model.py +163 -0
- blaxel/api/models/delete_model.py +154 -0
- blaxel/api/models/get_model.py +154 -0
- blaxel/api/models/get_model_logs.py +155 -0
- blaxel/api/models/get_model_metrics.py +158 -0
- blaxel/api/models/get_model_trace_ids.py +201 -0
- blaxel/api/models/list_model_revisions.py +158 -0
- blaxel/api/models/list_models.py +135 -0
- blaxel/api/models/update_model.py +176 -0
- blaxel/api/policies/__init__.py +0 -0
- blaxel/api/policies/create_policy.py +167 -0
- blaxel/api/policies/delete_policy.py +154 -0
- blaxel/api/policies/get_policy.py +154 -0
- blaxel/api/policies/list_policies.py +139 -0
- blaxel/api/policies/update_policy.py +180 -0
- blaxel/api/privateclusters/__init__.py +0 -0
- blaxel/api/privateclusters/create_private_cluster.py +132 -0
- blaxel/api/privateclusters/delete_private_cluster.py +156 -0
- blaxel/api/privateclusters/get_private_cluster.py +159 -0
- blaxel/api/privateclusters/get_private_cluster_health.py +97 -0
- blaxel/api/privateclusters/list_private_clusters.py +140 -0
- blaxel/api/privateclusters/update_private_cluster.py +156 -0
- blaxel/api/privateclusters/update_private_cluster_health.py +97 -0
- blaxel/api/service_accounts/__init__.py +0 -0
- blaxel/api/service_accounts/create_api_key_for_service_account.py +177 -0
- blaxel/api/service_accounts/create_workspace_service_account.py +170 -0
- blaxel/api/service_accounts/delete_api_key_for_service_account.py +104 -0
- blaxel/api/service_accounts/delete_workspace_service_account.py +160 -0
- blaxel/api/service_accounts/get_workspace_service_accounts.py +141 -0
- blaxel/api/service_accounts/list_api_keys_for_service_account.py +163 -0
- blaxel/api/service_accounts/update_workspace_service_account.py +183 -0
- blaxel/api/store/__init__.py +0 -0
- blaxel/api/store/get_store_agent.py +146 -0
- blaxel/api/store/get_store_function.py +146 -0
- blaxel/api/store/list_store_agents.py +131 -0
- blaxel/api/store/list_store_functions.py +131 -0
- blaxel/api/workspaces/__init__.py +0 -0
- blaxel/api/workspaces/accept_workspace_invitation.py +161 -0
- blaxel/api/workspaces/create_worspace.py +163 -0
- blaxel/api/workspaces/decline_workspace_invitation.py +158 -0
- blaxel/api/workspaces/delete_workspace.py +154 -0
- blaxel/api/workspaces/get_workspace.py +154 -0
- blaxel/api/workspaces/invite_workspace_user.py +174 -0
- blaxel/api/workspaces/leave_workspace.py +161 -0
- blaxel/api/workspaces/list_workspace_users.py +139 -0
- blaxel/api/workspaces/list_workspaces.py +139 -0
- blaxel/api/workspaces/remove_workspace_user.py +101 -0
- blaxel/api/workspaces/update_workspace.py +176 -0
- blaxel/api/workspaces/update_workspace_user_role.py +187 -0
- blaxel/authentication/__init__.py +45 -0
- blaxel/authentication/apikey.py +50 -0
- blaxel/authentication/authentication.py +176 -0
- blaxel/authentication/clientcredentials.py +103 -0
- blaxel/authentication/credentials.py +295 -0
- blaxel/authentication/device_mode.py +197 -0
- blaxel/client.py +281 -0
- blaxel/common/__init__.py +17 -0
- blaxel/common/error.py +27 -0
- blaxel/common/instrumentation.py +317 -0
- blaxel/common/logger.py +60 -0
- blaxel/common/secrets.py +39 -0
- blaxel/common/settings.py +150 -0
- blaxel/common/slugify.py +18 -0
- blaxel/common/utils.py +34 -0
- blaxel/deploy/__init__.py +8 -0
- blaxel/deploy/deploy.py +316 -0
- blaxel/deploy/format.py +46 -0
- blaxel/deploy/parser.py +192 -0
- blaxel/errors.py +16 -0
- blaxel/functions/__init__.py +7 -0
- blaxel/functions/common.py +228 -0
- blaxel/functions/decorator.py +64 -0
- blaxel/functions/local/local.py +48 -0
- blaxel/functions/mcp/client.py +96 -0
- blaxel/functions/mcp/mcp.py +168 -0
- blaxel/functions/mcp/utils.py +56 -0
- blaxel/functions/remote/remote.py +183 -0
- blaxel/models/__init__.py +233 -0
- blaxel/models/acl.py +133 -0
- blaxel/models/agent.py +126 -0
- blaxel/models/agent_chain.py +88 -0
- blaxel/models/agent_spec.py +346 -0
- blaxel/models/api_key.py +142 -0
- blaxel/models/configuration.py +85 -0
- blaxel/models/continent.py +70 -0
- blaxel/models/core_event.py +97 -0
- blaxel/models/core_spec.py +249 -0
- blaxel/models/core_spec_configurations.py +77 -0
- blaxel/models/country.py +70 -0
- blaxel/models/create_api_key_for_service_account_body.py +69 -0
- blaxel/models/create_workspace_service_account_body.py +71 -0
- blaxel/models/create_workspace_service_account_response_200.py +105 -0
- blaxel/models/delete_workspace_service_account_response_200.py +96 -0
- blaxel/models/entrypoint.py +96 -0
- blaxel/models/entrypoint_env.py +45 -0
- blaxel/models/flavor.py +70 -0
- blaxel/models/form.py +120 -0
- blaxel/models/form_config.py +45 -0
- blaxel/models/form_oauthomitempty.py +45 -0
- blaxel/models/form_secrets.py +45 -0
- blaxel/models/function.py +126 -0
- blaxel/models/function_kit.py +97 -0
- blaxel/models/function_spec.py +310 -0
- blaxel/models/get_trace_ids_response_200.py +45 -0
- blaxel/models/get_trace_logs_response_200.py +45 -0
- blaxel/models/get_trace_response_200.py +45 -0
- blaxel/models/get_workspace_service_accounts_response_200_item.py +96 -0
- blaxel/models/histogram_bucket.py +79 -0
- blaxel/models/histogram_stats.py +88 -0
- blaxel/models/integration_connection.py +96 -0
- blaxel/models/integration_connection_spec.py +114 -0
- blaxel/models/integration_connection_spec_config.py +45 -0
- blaxel/models/integration_connection_spec_secret.py +45 -0
- blaxel/models/integration_model.py +162 -0
- blaxel/models/integration_repository.py +88 -0
- blaxel/models/invite_workspace_user_body.py +60 -0
- blaxel/models/knowledgebase.py +126 -0
- blaxel/models/knowledgebase_spec.py +163 -0
- blaxel/models/knowledgebase_spec_options.py +45 -0
- blaxel/models/last_n_requests_metric.py +79 -0
- blaxel/models/latency_metric.py +144 -0
- blaxel/models/location_response.py +113 -0
- blaxel/models/mcp_definition.py +188 -0
- blaxel/models/mcp_definition_entrypoint.py +45 -0
- blaxel/models/mcp_definition_form.py +45 -0
- blaxel/models/metadata.py +139 -0
- blaxel/models/metadata_labels.py +45 -0
- blaxel/models/metric.py +79 -0
- blaxel/models/metrics.py +169 -0
- blaxel/models/metrics_models.py +45 -0
- blaxel/models/metrics_request_total_per_code.py +45 -0
- blaxel/models/metrics_rps_per_code.py +45 -0
- blaxel/models/model.py +126 -0
- blaxel/models/model_private_cluster.py +79 -0
- blaxel/models/model_spec.py +249 -0
- blaxel/models/o_auth.py +72 -0
- blaxel/models/owner_fields.py +70 -0
- blaxel/models/pending_invitation.py +124 -0
- blaxel/models/pending_invitation_accept.py +85 -0
- blaxel/models/pending_invitation_render.py +147 -0
- blaxel/models/pending_invitation_render_invited_by.py +88 -0
- blaxel/models/pending_invitation_render_workspace.py +70 -0
- blaxel/models/pending_invitation_workspace_details.py +72 -0
- blaxel/models/pod_template_spec.py +45 -0
- blaxel/models/policy.py +96 -0
- blaxel/models/policy_location.py +70 -0
- blaxel/models/policy_max_tokens.py +106 -0
- blaxel/models/policy_spec.py +151 -0
- blaxel/models/private_cluster.py +183 -0
- blaxel/models/private_location.py +61 -0
- blaxel/models/repository.py +70 -0
- blaxel/models/request_duration_over_time_metric.py +97 -0
- blaxel/models/request_duration_over_time_metrics.py +80 -0
- blaxel/models/request_total_by_origin_metric.py +115 -0
- blaxel/models/request_total_by_origin_metric_request_total_by_origin.py +45 -0
- blaxel/models/request_total_by_origin_metric_request_total_by_origin_and_code.py +45 -0
- blaxel/models/request_total_metric.py +123 -0
- blaxel/models/request_total_metric_request_total_per_code.py +45 -0
- blaxel/models/request_total_metric_rps_per_code.py +45 -0
- blaxel/models/resource_log.py +79 -0
- blaxel/models/resource_metrics.py +270 -0
- blaxel/models/resource_metrics_request_total_per_code.py +45 -0
- blaxel/models/resource_metrics_rps_per_code.py +45 -0
- blaxel/models/revision_configuration.py +97 -0
- blaxel/models/revision_metadata.py +124 -0
- blaxel/models/runtime.py +196 -0
- blaxel/models/runtime_startup_probe.py +45 -0
- blaxel/models/serverless_config.py +80 -0
- blaxel/models/spec_configuration.py +70 -0
- blaxel/models/store_agent.py +178 -0
- blaxel/models/store_agent_labels.py +45 -0
- blaxel/models/store_configuration.py +151 -0
- blaxel/models/store_configuration_option.py +79 -0
- blaxel/models/store_function.py +211 -0
- blaxel/models/store_function_kit.py +97 -0
- blaxel/models/store_function_labels.py +45 -0
- blaxel/models/store_function_parameter.py +88 -0
- blaxel/models/time_fields.py +70 -0
- blaxel/models/token_rate_metric.py +88 -0
- blaxel/models/token_rate_metrics.py +120 -0
- blaxel/models/token_total_metric.py +106 -0
- blaxel/models/trace_ids_response.py +45 -0
- blaxel/models/update_workspace_service_account_body.py +69 -0
- blaxel/models/update_workspace_service_account_response_200.py +96 -0
- blaxel/models/update_workspace_user_role_body.py +60 -0
- blaxel/models/websocket_channel.py +88 -0
- blaxel/models/workspace.py +148 -0
- blaxel/models/workspace_labels.py +45 -0
- blaxel/models/workspace_user.py +115 -0
- blaxel/py.typed +1 -0
- blaxel/run.py +108 -0
- blaxel/serve/app.py +131 -0
- blaxel/serve/middlewares/__init__.py +10 -0
- blaxel/serve/middlewares/accesslog.py +32 -0
- blaxel/serve/middlewares/processtime.py +28 -0
- blaxel/types.py +46 -0
- blaxel-0.64.0.dist-info/METADATA +96 -0
- blaxel-0.64.0.dist-info/RECORD +261 -0
- blaxel-0.64.0.dist-info/WHEEL +4 -0
- blaxel-0.64.0.dist-info/entry_points.txt +2 -0
- blaxel-0.64.0.dist-info/licenses/LICENSE +21 -0
blaxel/deploy/parser.py
ADDED
@@ -0,0 +1,192 @@
|
|
1
|
+
"""
|
2
|
+
This module provides classes and functions for parsing deployment resources within Blaxel.
|
3
|
+
It includes the Resource dataclass for representing deployment resources and functions to extract and process resources
|
4
|
+
decorated within Python files.
|
5
|
+
"""
|
6
|
+
|
7
|
+
import ast
|
8
|
+
import importlib
|
9
|
+
import os
|
10
|
+
from dataclasses import dataclass
|
11
|
+
from logging import getLogger
|
12
|
+
from typing import Callable, Literal
|
13
|
+
|
14
|
+
from blaxel.models import StoreFunctionParameter
|
15
|
+
|
16
|
+
|
17
|
+
@dataclass
|
18
|
+
class Resource:
|
19
|
+
"""
|
20
|
+
A dataclass representing a deployment resource.
|
21
|
+
|
22
|
+
Attributes:
|
23
|
+
type (Literal["agent", "function"]): The type of deployment ("agent" or "function").
|
24
|
+
module (Callable): The module containing the deployment.
|
25
|
+
name (str): The name of the deployment.
|
26
|
+
decorator (ast.Call): The decorator AST node used on the deployment function.
|
27
|
+
func (Callable): The deployment function.
|
28
|
+
"""
|
29
|
+
type: Literal["agent", "function"]
|
30
|
+
module: Callable
|
31
|
+
name: str
|
32
|
+
decorator: ast.Call
|
33
|
+
func: Callable
|
34
|
+
|
35
|
+
|
36
|
+
def get_resources(from_decorator, dir) -> list[Resource]:
|
37
|
+
"""
|
38
|
+
Scans through Python files in a directory to find functions decorated with a specific decorator.
|
39
|
+
|
40
|
+
Args:
|
41
|
+
from_decorator (str): The name of the decorator to search for
|
42
|
+
dir (str): The directory to scan, defaults to "src"
|
43
|
+
|
44
|
+
Returns:
|
45
|
+
list[Resource]: List of Resource objects containing information about decorated functions
|
46
|
+
"""
|
47
|
+
resources = []
|
48
|
+
logger = getLogger(__name__)
|
49
|
+
|
50
|
+
# Walk through all Python files in resources directory and subdirectories
|
51
|
+
for root, _, files in os.walk(dir):
|
52
|
+
for file in files:
|
53
|
+
if file.endswith(".py"):
|
54
|
+
file_path = os.path.join(root, file)
|
55
|
+
# Read and compile the file content
|
56
|
+
with open(file_path) as f:
|
57
|
+
try:
|
58
|
+
file_content = f.read()
|
59
|
+
# Parse the file content to find decorated resources
|
60
|
+
tree = ast.parse(file_content)
|
61
|
+
|
62
|
+
# Look for function definitions with decorators
|
63
|
+
for node in ast.walk(tree):
|
64
|
+
if (
|
65
|
+
not isinstance(node, ast.FunctionDef)
|
66
|
+
and not isinstance(node, ast.AsyncFunctionDef)
|
67
|
+
) or len(node.decorator_list) == 0:
|
68
|
+
continue
|
69
|
+
decorator = node.decorator_list[0]
|
70
|
+
|
71
|
+
decorator_name = ""
|
72
|
+
if isinstance(decorator, ast.Call):
|
73
|
+
decorator_name = decorator.func.id
|
74
|
+
if isinstance(decorator, ast.Name):
|
75
|
+
decorator_name = decorator.id
|
76
|
+
if decorator_name == from_decorator:
|
77
|
+
# Get the function name and decorator name
|
78
|
+
func_name = node.name
|
79
|
+
|
80
|
+
# Import the module to get the actual function
|
81
|
+
spec = importlib.util.spec_from_file_location(func_name, file_path)
|
82
|
+
module = importlib.util.module_from_spec(spec)
|
83
|
+
spec.loader.exec_module(module)
|
84
|
+
|
85
|
+
# Check if kit=True in the decorator arguments
|
86
|
+
|
87
|
+
# Get the decorated function
|
88
|
+
if hasattr(module, func_name) and isinstance(decorator, ast.Call):
|
89
|
+
resources.append(
|
90
|
+
Resource(
|
91
|
+
type=decorator_name,
|
92
|
+
module=module,
|
93
|
+
name=func_name,
|
94
|
+
func=getattr(module, func_name),
|
95
|
+
decorator=decorator,
|
96
|
+
)
|
97
|
+
)
|
98
|
+
except Exception as e:
|
99
|
+
logger.warning(f"Error processing {file_path}: {e!s} at line {e.__traceback__.tb_lineno}")
|
100
|
+
return resources
|
101
|
+
|
102
|
+
|
103
|
+
def get_parameters(resource: Resource) -> list[StoreFunctionParameter]:
|
104
|
+
"""
|
105
|
+
Extracts parameter information from a function's signature and docstring.
|
106
|
+
|
107
|
+
Args:
|
108
|
+
resource (Resource): The resource object containing the function to analyze
|
109
|
+
|
110
|
+
Returns:
|
111
|
+
list[StoreFunctionParameter]: List of parameter objects with name, type, required status, and description
|
112
|
+
"""
|
113
|
+
parameters = []
|
114
|
+
# Get function signature
|
115
|
+
import inspect
|
116
|
+
|
117
|
+
sig = inspect.signature(resource.func)
|
118
|
+
# Get docstring for parameter descriptions
|
119
|
+
docstring = inspect.getdoc(resource.func)
|
120
|
+
param_descriptions = {}
|
121
|
+
if docstring:
|
122
|
+
# Parse docstring for parameter descriptions
|
123
|
+
lines = docstring.split("\n")
|
124
|
+
for line in lines:
|
125
|
+
line = line.strip().lower()
|
126
|
+
if line.startswith(":param "):
|
127
|
+
# Extract parameter name and description
|
128
|
+
param_line = line[7:].split(":", 1)
|
129
|
+
if len(param_line) == 2:
|
130
|
+
param_name = param_line[0].strip()
|
131
|
+
param_desc = param_line[1].strip()
|
132
|
+
param_descriptions[param_name] = param_desc
|
133
|
+
for name, param in sig.parameters.items():
|
134
|
+
# Skip *args and **kwargs parameters
|
135
|
+
if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
|
136
|
+
continue
|
137
|
+
|
138
|
+
param_type = "string" # Default type
|
139
|
+
type_mapping = {
|
140
|
+
"str": "string",
|
141
|
+
"int": "integer",
|
142
|
+
"float": "number",
|
143
|
+
"bool": "boolean",
|
144
|
+
"list": "array",
|
145
|
+
"dict": "object",
|
146
|
+
"none": "null",
|
147
|
+
}
|
148
|
+
if param.annotation != inspect.Parameter.empty:
|
149
|
+
# Map Python types to OpenAPI types
|
150
|
+
if hasattr(param.annotation, "__name__"):
|
151
|
+
param_type = param.annotation.__name__.lower()
|
152
|
+
else:
|
153
|
+
# Handle special types like Union, Optional etc
|
154
|
+
param_type = str(param.annotation).lower()
|
155
|
+
parameter = StoreFunctionParameter(
|
156
|
+
name=name,
|
157
|
+
type_=type_mapping.get(param_type, "string"),
|
158
|
+
required=param.default == inspect.Parameter.empty,
|
159
|
+
description=param_descriptions.get(name, f"Parameter {name}"),
|
160
|
+
)
|
161
|
+
parameters.append(parameter)
|
162
|
+
|
163
|
+
return parameters
|
164
|
+
|
165
|
+
|
166
|
+
def get_description(description: str | None, resource: Resource) -> str:
|
167
|
+
"""
|
168
|
+
Gets the description of a function from either a provided description or the function's docstring.
|
169
|
+
|
170
|
+
Args:
|
171
|
+
description (str | None): Optional explicit description
|
172
|
+
resource (Resource): The resource object containing the function
|
173
|
+
|
174
|
+
Returns:
|
175
|
+
str: The function description
|
176
|
+
"""
|
177
|
+
if description:
|
178
|
+
return description
|
179
|
+
doc = resource.func.__doc__
|
180
|
+
if doc:
|
181
|
+
# Split docstring into sections and get only the description part
|
182
|
+
doc_lines = doc.split("\n")
|
183
|
+
description_lines = []
|
184
|
+
for line in doc_lines:
|
185
|
+
line = line.strip()
|
186
|
+
# Stop when we hit param/return sections
|
187
|
+
if line.startswith(":param") or line.startswith(":return"):
|
188
|
+
break
|
189
|
+
if line:
|
190
|
+
description_lines.append(line)
|
191
|
+
return " ".join(description_lines).strip()
|
192
|
+
return ""
|
blaxel/errors.py
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
"""Contains shared errors types that can be raised from API functions"""
|
2
|
+
|
3
|
+
|
4
|
+
class UnexpectedStatus(Exception):
|
5
|
+
"""Raised by api functions when the response status an undocumented status and Client.raise_on_unexpected_status is True"""
|
6
|
+
|
7
|
+
def __init__(self, status_code: int, content: bytes):
|
8
|
+
self.status_code = status_code
|
9
|
+
self.content = content
|
10
|
+
|
11
|
+
super().__init__(
|
12
|
+
f"Unexpected status code: {status_code}\n\nResponse content:\n{content.decode(errors='ignore')}"
|
13
|
+
)
|
14
|
+
|
15
|
+
|
16
|
+
__all__ = ["UnexpectedStatus"]
|
@@ -0,0 +1,7 @@
|
|
1
|
+
"""Functions package providing function decorators and utilities for Blaxel integration.
|
2
|
+
It includes decorators for creating function tools and utilities for managing and retrieving functions."""
|
3
|
+
|
4
|
+
from .common import get_functions
|
5
|
+
from .decorator import function, kit
|
6
|
+
|
7
|
+
__all__ = ["function", "kit", "get_functions"]
|
@@ -0,0 +1,228 @@
|
|
1
|
+
"""Decorators for creating function tools with Blaxel and LangChain integration.
|
2
|
+
|
3
|
+
This module provides functionality to discover and load function tools from Python files,
|
4
|
+
supporting both local and remote function execution.
|
5
|
+
|
6
|
+
Key Features:
|
7
|
+
- Automatic function discovery in specified directories
|
8
|
+
- Support for both synchronous and asynchronous functions
|
9
|
+
- Integration with LangChain's StructuredTool system
|
10
|
+
- Remote function toolkit handling
|
11
|
+
- Chain toolkit integration
|
12
|
+
|
13
|
+
Main Components:
|
14
|
+
- get_functions(): Core function that discovers and loads function tools
|
15
|
+
"""
|
16
|
+
import ast
|
17
|
+
import asyncio
|
18
|
+
import importlib.util
|
19
|
+
import os
|
20
|
+
import traceback
|
21
|
+
from logging import getLogger
|
22
|
+
from typing import Union
|
23
|
+
|
24
|
+
from langchain_core.tools import StructuredTool
|
25
|
+
from langchain_core.tools.base import create_schema_from_function
|
26
|
+
|
27
|
+
from blaxel.aimon import slugify
|
28
|
+
from blaxel.aimon.settings import get_settings
|
29
|
+
from blaxel.authentication import new_client
|
30
|
+
from blaxel.client import AuthenticatedClient
|
31
|
+
from blaxel.functions.local.local import LocalToolKit
|
32
|
+
from blaxel.functions.remote.remote import RemoteToolkit
|
33
|
+
from blaxel.models import AgentChain
|
34
|
+
|
35
|
+
logger = getLogger(__name__)
|
36
|
+
|
37
|
+
MAX_RETRIES = 10
|
38
|
+
RETRY_DELAY = 1 # 1 second delay between retries
|
39
|
+
|
40
|
+
async def initialize_with_retry(toolkit, function_name: str, max_retries: int):
|
41
|
+
for attempt in range(1, max_retries + 1):
|
42
|
+
try:
|
43
|
+
await toolkit.initialize()
|
44
|
+
return await toolkit.get_tools()
|
45
|
+
except Exception as e:
|
46
|
+
if attempt == max_retries:
|
47
|
+
logger.warn(f"Failed to initialize function {function_name} after {max_retries} attempts: {e!s}")
|
48
|
+
raise
|
49
|
+
logger.info(f"Attempt {attempt} failed for {function_name}, retrying...")
|
50
|
+
await asyncio.sleep(RETRY_DELAY)
|
51
|
+
return []
|
52
|
+
|
53
|
+
async def get_functions(
|
54
|
+
remote_functions: Union[list[str], None] = None,
|
55
|
+
local_functions: Union[list[dict], None] = None,
|
56
|
+
client: Union[AuthenticatedClient, None] = None,
|
57
|
+
dir: Union[str, None] = None,
|
58
|
+
chain: Union[list[AgentChain], None] = None,
|
59
|
+
remote_functions_empty: bool = True,
|
60
|
+
local_functions_empty: bool = True,
|
61
|
+
from_decorator: str = "function",
|
62
|
+
warning: bool = True,
|
63
|
+
):
|
64
|
+
"""Discovers and loads function tools from Python files and remote sources.
|
65
|
+
|
66
|
+
This function walks through Python files in a specified directory, looking for
|
67
|
+
decorated functions to convert into LangChain tools. It also handles remote
|
68
|
+
functions and chain toolkits.
|
69
|
+
|
70
|
+
Args:
|
71
|
+
remote_functions (Union[list[str], None]): List of remote function names to load
|
72
|
+
client (Union[AuthenticatedClient, None]): Authenticated client instance for API calls
|
73
|
+
dir (Union[str, None]): Directory to search for Python files containing functions
|
74
|
+
chain (Union[list[AgentChain], None]): List of agent chains to include
|
75
|
+
remote_functions_empty (bool): Whether to allow empty remote functions
|
76
|
+
from_decorator (str): Name of the decorator to look for (default: "function")
|
77
|
+
warning (bool): Whether to show warning messages
|
78
|
+
|
79
|
+
Returns:
|
80
|
+
list: List of discovered and loaded function tools
|
81
|
+
|
82
|
+
The function performs the following steps:
|
83
|
+
1. Walks through Python files in the specified directory
|
84
|
+
2. Parses each file to find decorated functions
|
85
|
+
3. Converts found functions into LangChain StructuredTools
|
86
|
+
4. Handles both synchronous and asynchronous functions
|
87
|
+
5. Processes remote functions if specified
|
88
|
+
6. Integrates chain toolkits if provided
|
89
|
+
|
90
|
+
Example:
|
91
|
+
```python
|
92
|
+
tools = get_functions(
|
93
|
+
dir="./functions",
|
94
|
+
from_decorator="function",
|
95
|
+
warning=True
|
96
|
+
)
|
97
|
+
```
|
98
|
+
"""
|
99
|
+
from blaxel.agents.chain import ChainToolkit
|
100
|
+
|
101
|
+
settings = get_settings()
|
102
|
+
if client is None:
|
103
|
+
client = new_client()
|
104
|
+
if dir is None:
|
105
|
+
dir = settings.agent.functions_directory
|
106
|
+
|
107
|
+
functions = []
|
108
|
+
logger = getLogger(__name__)
|
109
|
+
settings = get_settings()
|
110
|
+
|
111
|
+
# Walk through all Python files in functions directory and subdirectories
|
112
|
+
if not os.path.exists(dir):
|
113
|
+
if remote_functions_empty and warning:
|
114
|
+
logger.warn(f"Functions directory {dir} not found")
|
115
|
+
if os.path.exists(dir):
|
116
|
+
for root, _, files in os.walk(dir):
|
117
|
+
for file in files:
|
118
|
+
if file.endswith(".py"):
|
119
|
+
file_path = os.path.join(root, file)
|
120
|
+
# Read and compile the file content
|
121
|
+
with open(file_path) as f:
|
122
|
+
try:
|
123
|
+
file_content = f.read()
|
124
|
+
# Parse the file content to find decorated functions
|
125
|
+
tree = ast.parse(file_content)
|
126
|
+
|
127
|
+
# Look for function definitions with decorators
|
128
|
+
for node in ast.walk(tree):
|
129
|
+
if (
|
130
|
+
not isinstance(node, ast.FunctionDef)
|
131
|
+
and not isinstance(node, ast.AsyncFunctionDef)
|
132
|
+
) or len(node.decorator_list) == 0:
|
133
|
+
continue
|
134
|
+
decorator = node.decorator_list[0]
|
135
|
+
|
136
|
+
decorator_name = ""
|
137
|
+
if isinstance(decorator, ast.Call):
|
138
|
+
decorator_name = decorator.func.id
|
139
|
+
if isinstance(decorator, ast.Name):
|
140
|
+
decorator_name = decorator.id
|
141
|
+
if decorator_name == from_decorator:
|
142
|
+
# Get the function name and decorator name
|
143
|
+
func_name = node.name
|
144
|
+
|
145
|
+
# Import the module to get the actual function
|
146
|
+
spec = importlib.util.spec_from_file_location(func_name, file_path)
|
147
|
+
module = importlib.util.module_from_spec(spec)
|
148
|
+
spec.loader.exec_module(module)
|
149
|
+
# Check if kit=True in the decorator arguments
|
150
|
+
is_kit = False
|
151
|
+
if isinstance(decorator, ast.Call):
|
152
|
+
for keyword in decorator.keywords:
|
153
|
+
if keyword.arg == "kit" and isinstance(
|
154
|
+
keyword.value, ast.Constant
|
155
|
+
):
|
156
|
+
is_kit = keyword.value.value
|
157
|
+
if is_kit and not settings.remote:
|
158
|
+
kit_functions = await get_functions(
|
159
|
+
client=client,
|
160
|
+
dir=os.path.join(root),
|
161
|
+
remote_functions_empty=remote_functions_empty,
|
162
|
+
local_functions_empty=local_functions_empty,
|
163
|
+
from_decorator="kit",
|
164
|
+
)
|
165
|
+
functions.extend(kit_functions)
|
166
|
+
|
167
|
+
# Get the decorated function
|
168
|
+
if not is_kit and hasattr(module, func_name):
|
169
|
+
func = getattr(module, func_name)
|
170
|
+
if settings.remote:
|
171
|
+
toolkit = RemoteToolkit(client, slugify(func.__name__))
|
172
|
+
await toolkit.initialize()
|
173
|
+
functions.extend(await toolkit.get_tools())
|
174
|
+
else:
|
175
|
+
if asyncio.iscoroutinefunction(func):
|
176
|
+
functions.append(
|
177
|
+
StructuredTool(
|
178
|
+
name=func.__name__,
|
179
|
+
description=func.__doc__,
|
180
|
+
func=func,
|
181
|
+
coroutine=func,
|
182
|
+
args_schema=create_schema_from_function(func.__name__, func)
|
183
|
+
)
|
184
|
+
)
|
185
|
+
else:
|
186
|
+
|
187
|
+
functions.append(
|
188
|
+
StructuredTool(
|
189
|
+
name=func.__name__,
|
190
|
+
description=func.__doc__,
|
191
|
+
func=func,
|
192
|
+
args_schema=create_schema_from_function(func.__name__, func)
|
193
|
+
)
|
194
|
+
)
|
195
|
+
except Exception as e:
|
196
|
+
logger.warning(f"Error processing {file_path}: {e!s}")
|
197
|
+
if remote_functions:
|
198
|
+
for function in remote_functions:
|
199
|
+
try:
|
200
|
+
toolkit = RemoteToolkit(client, function)
|
201
|
+
tools = await initialize_with_retry(toolkit, function, MAX_RETRIES)
|
202
|
+
functions.extend(tools)
|
203
|
+
except Exception as e:
|
204
|
+
if not isinstance(e, RuntimeError):
|
205
|
+
logger.debug(
|
206
|
+
f"Failed to initialize remote function {function}: {e!s}\n"
|
207
|
+
f"Traceback:\n{traceback.format_exc()}"
|
208
|
+
)
|
209
|
+
logger.warn(f"Failed to initialize remote function {function}: {e!s}")
|
210
|
+
if local_functions:
|
211
|
+
for function in local_functions:
|
212
|
+
try:
|
213
|
+
toolkit = LocalToolKit(client, function)
|
214
|
+
tools = await initialize_with_retry(toolkit, function["name"], MAX_RETRIES)
|
215
|
+
functions.extend(tools)
|
216
|
+
except Exception as e:
|
217
|
+
logger.debug(
|
218
|
+
f"Failed to initialize local function {function}: {e!s}\n"
|
219
|
+
f"Traceback:\n{traceback.format_exc()}"
|
220
|
+
)
|
221
|
+
logger.warn(f"Failed to initialize local function {function}: {e!s}")
|
222
|
+
|
223
|
+
if chain:
|
224
|
+
toolkit = ChainToolkit(client, chain)
|
225
|
+
await toolkit.initialize()
|
226
|
+
functions.extend(await toolkit.get_tools())
|
227
|
+
return functions
|
228
|
+
|
@@ -0,0 +1,64 @@
|
|
1
|
+
"""Decorators for creating function tools with Blaxel and LangChain integration."""
|
2
|
+
import asyncio
|
3
|
+
import functools
|
4
|
+
from collections.abc import Callable
|
5
|
+
from logging import getLogger
|
6
|
+
|
7
|
+
from fastapi import Request
|
8
|
+
|
9
|
+
from blaxel.models import Function, FunctionKit
|
10
|
+
|
11
|
+
logger = getLogger(__name__)
|
12
|
+
|
13
|
+
def kit(bl_kit: FunctionKit = None, **kwargs: dict) -> Callable:
|
14
|
+
"""
|
15
|
+
Decorator to create function tools with Blaxel and LangChain integration.
|
16
|
+
|
17
|
+
Args:
|
18
|
+
bl_kit (FunctionKit | None): Optional FunctionKit to associate with the function.
|
19
|
+
**kwargs (dict): Additional keyword arguments for function configuration.
|
20
|
+
|
21
|
+
Returns:
|
22
|
+
Callable: The decorated function.
|
23
|
+
"""
|
24
|
+
|
25
|
+
def wrapper(func: Callable) -> Callable:
|
26
|
+
if bl_kit and not func.__doc__ and bl_kit.description:
|
27
|
+
func.__doc__ = bl_kit.description
|
28
|
+
return func
|
29
|
+
|
30
|
+
return wrapper
|
31
|
+
|
32
|
+
|
33
|
+
def function(*args, function: Function | dict = None, kit=False, **kwargs: dict) -> Callable:
|
34
|
+
"""
|
35
|
+
Decorator to create function tools with Blaxel and LangChain integration.
|
36
|
+
|
37
|
+
Args:
|
38
|
+
function (Function | dict): Function metadata or a dictionary representing it.
|
39
|
+
kit (bool): Whether to associate a function kit.
|
40
|
+
**kwargs (dict): Additional keyword arguments for function configuration.
|
41
|
+
|
42
|
+
Returns:
|
43
|
+
Callable: The decorated function.
|
44
|
+
"""
|
45
|
+
if function is not None and not isinstance(function, dict):
|
46
|
+
raise Exception(
|
47
|
+
'function must be a dictionary, example: @function(function={"metadata": {"name": "my_function"}})'
|
48
|
+
)
|
49
|
+
if isinstance(function, dict):
|
50
|
+
function = Function(**function)
|
51
|
+
|
52
|
+
def wrapper(func: Callable) -> Callable:
|
53
|
+
if function and not func.__doc__ and function.spec and function.spec.description:
|
54
|
+
func.__doc__ = function.spec.description
|
55
|
+
|
56
|
+
@functools.wraps(func)
|
57
|
+
async def wrapped(*args, **kwargs):
|
58
|
+
if len(args) > 0 and isinstance(args[0], Request):
|
59
|
+
body = await args[0].json()
|
60
|
+
args = [body.get(param) for param in func.__code__.co_varnames[:func.__code__.co_argcount]]
|
61
|
+
return await func(*args, **kwargs) if asyncio.iscoroutinefunction(func) else func(*args, **kwargs)
|
62
|
+
return wrapped
|
63
|
+
|
64
|
+
return wrapper
|
@@ -0,0 +1,48 @@
|
|
1
|
+
from dataclasses import dataclass
|
2
|
+
|
3
|
+
import pydantic
|
4
|
+
from langchain_core.tools.base import BaseTool
|
5
|
+
|
6
|
+
from blaxel.authentication.authentication import AuthenticatedClient
|
7
|
+
from blaxel.functions.mcp.mcp import MCPClient, MCPToolkit
|
8
|
+
from blaxel.models import Function
|
9
|
+
|
10
|
+
|
11
|
+
@dataclass
|
12
|
+
class LocalToolKit:
|
13
|
+
"""
|
14
|
+
Toolkit for managing local tools.
|
15
|
+
|
16
|
+
Attributes:
|
17
|
+
client (AuthenticatedClient): The authenticated client instance.
|
18
|
+
function (str): The name of the local function to integrate.
|
19
|
+
_function (Function | None): Cached Function object after initialization.
|
20
|
+
"""
|
21
|
+
client: AuthenticatedClient
|
22
|
+
local_function: dict
|
23
|
+
_function: Function | None = None
|
24
|
+
model_config = pydantic.ConfigDict(arbitrary_types_allowed=True)
|
25
|
+
|
26
|
+
async def initialize(self) -> None:
|
27
|
+
"""Initialize the session and retrieve the local function details."""
|
28
|
+
if self._function is None:
|
29
|
+
try:
|
30
|
+
# For local functions, we directly create the Function object
|
31
|
+
# based on the local function name
|
32
|
+
self._function = Function(
|
33
|
+
metadata={"name": self.local_function['name']},
|
34
|
+
spec={
|
35
|
+
"configurations": {
|
36
|
+
"url": self.local_function['url'],
|
37
|
+
},
|
38
|
+
"description": self.local_function['description'] or "",
|
39
|
+
}
|
40
|
+
)
|
41
|
+
except Exception as e:
|
42
|
+
raise RuntimeError(f"Failed to initialize local function: {e}")
|
43
|
+
|
44
|
+
async def get_tools(self) -> list[BaseTool]:
|
45
|
+
mcp_client = MCPClient(self.client, self._function.spec["configurations"]["url"], None)
|
46
|
+
mcp_toolkit = MCPToolkit(client=mcp_client, url=self._function.spec["configurations"]["url"])
|
47
|
+
await mcp_toolkit.initialize()
|
48
|
+
return await mcp_toolkit.get_tools()
|
@@ -0,0 +1,96 @@
|
|
1
|
+
import logging
|
2
|
+
from contextlib import asynccontextmanager
|
3
|
+
from typing import Any
|
4
|
+
from urllib.parse import urljoin, urlparse
|
5
|
+
|
6
|
+
import anyio
|
7
|
+
import mcp.types as types
|
8
|
+
from anyio.abc import TaskStatus
|
9
|
+
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
|
10
|
+
from websockets.client import WebSocketClientProtocol
|
11
|
+
from websockets.client import connect as ws_connect
|
12
|
+
|
13
|
+
logger = logging.getLogger(__name__)
|
14
|
+
|
15
|
+
|
16
|
+
def remove_request_params(url: str) -> str:
|
17
|
+
return urljoin(url, urlparse(url).path)
|
18
|
+
|
19
|
+
|
20
|
+
@asynccontextmanager
|
21
|
+
async def websocket_client(
|
22
|
+
url: str,
|
23
|
+
headers: dict[str, Any] | None = None,
|
24
|
+
timeout: float = 5,
|
25
|
+
):
|
26
|
+
"""
|
27
|
+
Client transport for WebSocket.
|
28
|
+
|
29
|
+
The `timeout` parameter controls connection timeout.
|
30
|
+
"""
|
31
|
+
read_stream: MemoryObjectReceiveStream[types.JSONRPCMessage | Exception]
|
32
|
+
read_stream_writer: MemoryObjectSendStream[types.JSONRPCMessage | Exception]
|
33
|
+
|
34
|
+
write_stream: MemoryObjectSendStream[types.JSONRPCMessage]
|
35
|
+
write_stream_reader: MemoryObjectReceiveStream[types.JSONRPCMessage]
|
36
|
+
|
37
|
+
read_stream_writer, read_stream = anyio.create_memory_object_stream(0)
|
38
|
+
write_stream, write_stream_reader = anyio.create_memory_object_stream(0)
|
39
|
+
|
40
|
+
async with anyio.create_task_group() as tg:
|
41
|
+
try:
|
42
|
+
# Convert http(s):// to ws(s)://
|
43
|
+
ws_url = url.replace("http://", "ws://").replace("https://", "wss://")
|
44
|
+
logger.debug(f"Connecting to WebSocket endpoint: {remove_request_params(ws_url)}")
|
45
|
+
|
46
|
+
async with ws_connect(ws_url, extra_headers=headers, open_timeout=timeout) as websocket:
|
47
|
+
logger.debug("WebSocket connection established")
|
48
|
+
|
49
|
+
async def ws_reader(
|
50
|
+
websocket: WebSocketClientProtocol,
|
51
|
+
task_status: TaskStatus[None] = anyio.TASK_STATUS_IGNORED,
|
52
|
+
):
|
53
|
+
try:
|
54
|
+
task_status.started()
|
55
|
+
async for message in websocket:
|
56
|
+
logger.debug(f"Received WebSocket message: {message}")
|
57
|
+
try:
|
58
|
+
parsed_message = types.JSONRPCMessage.model_validate_json(message)
|
59
|
+
logger.debug(f"Received server message: {parsed_message}")
|
60
|
+
await read_stream_writer.send(parsed_message)
|
61
|
+
except Exception as exc:
|
62
|
+
logger.error(f"Error parsing server message: {exc}")
|
63
|
+
await read_stream_writer.send(exc)
|
64
|
+
except Exception as exc:
|
65
|
+
logger.error(f"Error in ws_reader: {exc}")
|
66
|
+
await read_stream_writer.send(exc)
|
67
|
+
finally:
|
68
|
+
await read_stream_writer.aclose()
|
69
|
+
|
70
|
+
async def ws_writer(websocket: WebSocketClientProtocol):
|
71
|
+
try:
|
72
|
+
async with write_stream_reader:
|
73
|
+
async for message in write_stream_reader:
|
74
|
+
logger.debug(f"Sending client message: {message}")
|
75
|
+
await websocket.send(
|
76
|
+
message.model_dump_json(
|
77
|
+
by_alias=True,
|
78
|
+
exclude_none=True,
|
79
|
+
)
|
80
|
+
)
|
81
|
+
logger.debug("Client message sent successfully")
|
82
|
+
except Exception as exc:
|
83
|
+
logger.error(f"Error in ws_writer: {exc}")
|
84
|
+
finally:
|
85
|
+
await write_stream.aclose()
|
86
|
+
|
87
|
+
await tg.start(ws_reader, websocket)
|
88
|
+
tg.start_soon(ws_writer, websocket)
|
89
|
+
|
90
|
+
try:
|
91
|
+
yield read_stream, write_stream
|
92
|
+
finally:
|
93
|
+
tg.cancel_scope.cancel()
|
94
|
+
finally:
|
95
|
+
await read_stream_writer.aclose()
|
96
|
+
await write_stream.aclose()
|