arcade-core 2.3.0__py3-none-any.whl → 2.5.0rc1__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.
- arcade_core/catalog.py +97 -38
- arcade_core/context.py +128 -0
- arcade_core/converters/openai.py +220 -0
- arcade_core/discovery.py +253 -0
- arcade_core/errors.py +310 -35
- arcade_core/executor.py +10 -17
- arcade_core/output.py +45 -9
- arcade_core/parse.py +12 -0
- arcade_core/schema.py +82 -20
- arcade_core/toolkit.py +74 -3
- arcade_core/utils.py +4 -1
- {arcade_core-2.3.0.dist-info → arcade_core-2.5.0rc1.dist-info}/METADATA +1 -4
- arcade_core-2.5.0rc1.dist-info/RECORD +21 -0
- arcade_core/telemetry.py +0 -130
- arcade_core-2.3.0.dist-info/RECORD +0 -19
- {arcade_core-2.3.0.dist-info → arcade_core-2.5.0rc1.dist-info}/WHEEL +0 -0
arcade_core/executor.py
CHANGED
|
@@ -6,11 +6,9 @@ from typing import Any
|
|
|
6
6
|
from pydantic import BaseModel, ValidationError
|
|
7
7
|
|
|
8
8
|
from arcade_core.errors import (
|
|
9
|
-
RetryableToolError,
|
|
10
9
|
ToolInputError,
|
|
11
10
|
ToolOutputError,
|
|
12
11
|
ToolRuntimeError,
|
|
13
|
-
ToolSerializationError,
|
|
14
12
|
)
|
|
15
13
|
from arcade_core.output import output_factory
|
|
16
14
|
from arcade_core.schema import (
|
|
@@ -69,31 +67,26 @@ class ToolExecutor:
|
|
|
69
67
|
# return the output
|
|
70
68
|
return output_factory.success(data=output, logs=tool_call_logs)
|
|
71
69
|
|
|
72
|
-
except RetryableToolError as e:
|
|
73
|
-
return output_factory.fail_retry(
|
|
74
|
-
message=e.message,
|
|
75
|
-
developer_message=e.developer_message,
|
|
76
|
-
additional_prompt_content=e.additional_prompt_content,
|
|
77
|
-
retry_after_ms=e.retry_after_ms,
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
except ToolSerializationError as e:
|
|
81
|
-
return output_factory.fail(message=e.message, developer_message=e.developer_message)
|
|
82
|
-
|
|
83
|
-
# should catch all tool exceptions due to the try/except in the tool decorator
|
|
84
70
|
except ToolRuntimeError as e:
|
|
71
|
+
e.with_context(func.__name__)
|
|
85
72
|
return output_factory.fail(
|
|
86
73
|
message=e.message,
|
|
87
74
|
developer_message=e.developer_message,
|
|
88
|
-
|
|
75
|
+
stacktrace=e.stacktrace(),
|
|
76
|
+
additional_prompt_content=getattr(e, "additional_prompt_content", None),
|
|
77
|
+
retry_after_ms=getattr(e, "retry_after_ms", None),
|
|
78
|
+
kind=e.kind,
|
|
79
|
+
can_retry=e.can_retry,
|
|
80
|
+
status_code=e.status_code,
|
|
81
|
+
extra=e.extra,
|
|
89
82
|
)
|
|
90
83
|
|
|
91
84
|
# if we get here we're in trouble
|
|
92
85
|
except Exception as e:
|
|
93
86
|
return output_factory.fail(
|
|
94
|
-
message="Error in execution",
|
|
87
|
+
message=f"Error in execution of '{func.__name__}'",
|
|
95
88
|
developer_message=str(e),
|
|
96
|
-
|
|
89
|
+
stacktrace=traceback.format_exc(),
|
|
97
90
|
)
|
|
98
91
|
|
|
99
92
|
@staticmethod
|
arcade_core/output.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
from typing import TypeVar
|
|
1
|
+
from typing import Any, TypeVar
|
|
2
2
|
|
|
3
3
|
from pydantic import BaseModel
|
|
4
4
|
|
|
5
|
+
from arcade_core.errors import ErrorKind
|
|
5
6
|
from arcade_core.schema import ToolCallError, ToolCallLog, ToolCallOutput
|
|
6
7
|
from arcade_core.utils import coerce_empty_list_to_none
|
|
7
8
|
|
|
@@ -25,14 +26,27 @@ class ToolOutputFactory:
|
|
|
25
26
|
|
|
26
27
|
The executor guarantees that `data` is either a string, a dict, or None.
|
|
27
28
|
"""
|
|
28
|
-
value: str | int | float | bool | dict | list
|
|
29
|
+
value: str | int | float | bool | dict | list | None
|
|
29
30
|
if data is None:
|
|
30
31
|
value = ""
|
|
31
32
|
elif hasattr(data, "result"):
|
|
32
|
-
|
|
33
|
+
result = getattr(data, "result", "")
|
|
34
|
+
# Handle None result the same way as None data
|
|
35
|
+
if result is None:
|
|
36
|
+
value = ""
|
|
37
|
+
# If the result is a BaseModel (e.g., from TypedDict conversion), convert to dict
|
|
38
|
+
elif isinstance(result, BaseModel):
|
|
39
|
+
value = result.model_dump()
|
|
40
|
+
# If the result is a list, check if it contains BaseModel objects
|
|
41
|
+
elif isinstance(result, list):
|
|
42
|
+
value = [
|
|
43
|
+
item.model_dump() if isinstance(item, BaseModel) else item for item in result
|
|
44
|
+
]
|
|
45
|
+
else:
|
|
46
|
+
value = result
|
|
33
47
|
elif isinstance(data, BaseModel):
|
|
34
48
|
value = data.model_dump()
|
|
35
|
-
elif isinstance(data, (str, int, float, bool, list)):
|
|
49
|
+
elif isinstance(data, (str, int, float, bool, list, dict)):
|
|
36
50
|
value = data
|
|
37
51
|
else:
|
|
38
52
|
raise ValueError(f"Unsupported data output type: {type(data)}")
|
|
@@ -48,15 +62,26 @@ class ToolOutputFactory:
|
|
|
48
62
|
*,
|
|
49
63
|
message: str,
|
|
50
64
|
developer_message: str | None = None,
|
|
51
|
-
|
|
65
|
+
stacktrace: str | None = None,
|
|
52
66
|
logs: list[ToolCallLog] | None = None,
|
|
67
|
+
additional_prompt_content: str | None = None,
|
|
68
|
+
retry_after_ms: int | None = None,
|
|
69
|
+
kind: ErrorKind = ErrorKind.UNKNOWN,
|
|
70
|
+
can_retry: bool = False,
|
|
71
|
+
status_code: int | None = None,
|
|
72
|
+
extra: dict[str, Any] | None = None,
|
|
53
73
|
) -> ToolCallOutput:
|
|
54
74
|
return ToolCallOutput(
|
|
55
75
|
error=ToolCallError(
|
|
56
76
|
message=message,
|
|
57
77
|
developer_message=developer_message,
|
|
58
|
-
can_retry=
|
|
59
|
-
|
|
78
|
+
can_retry=can_retry,
|
|
79
|
+
additional_prompt_content=additional_prompt_content,
|
|
80
|
+
retry_after_ms=retry_after_ms,
|
|
81
|
+
stacktrace=stacktrace,
|
|
82
|
+
kind=kind,
|
|
83
|
+
status_code=status_code,
|
|
84
|
+
extra=extra,
|
|
60
85
|
),
|
|
61
86
|
logs=coerce_empty_list_to_none(logs),
|
|
62
87
|
)
|
|
@@ -68,9 +93,17 @@ class ToolOutputFactory:
|
|
|
68
93
|
developer_message: str | None = None,
|
|
69
94
|
additional_prompt_content: str | None = None,
|
|
70
95
|
retry_after_ms: int | None = None,
|
|
71
|
-
|
|
96
|
+
stacktrace: str | None = None,
|
|
72
97
|
logs: list[ToolCallLog] | None = None,
|
|
98
|
+
kind: ErrorKind = ErrorKind.TOOL_RUNTIME_RETRY,
|
|
99
|
+
status_code: int = 500,
|
|
100
|
+
extra: dict[str, Any] | None = None,
|
|
73
101
|
) -> ToolCallOutput:
|
|
102
|
+
"""
|
|
103
|
+
DEPRECATED: Use ToolOutputFactory.fail instead.
|
|
104
|
+
This method will be removed in version 3.0.0
|
|
105
|
+
"""
|
|
106
|
+
|
|
74
107
|
return ToolCallOutput(
|
|
75
108
|
error=ToolCallError(
|
|
76
109
|
message=message,
|
|
@@ -78,7 +111,10 @@ class ToolOutputFactory:
|
|
|
78
111
|
can_retry=True,
|
|
79
112
|
additional_prompt_content=additional_prompt_content,
|
|
80
113
|
retry_after_ms=retry_after_ms,
|
|
81
|
-
|
|
114
|
+
stacktrace=stacktrace,
|
|
115
|
+
kind=kind,
|
|
116
|
+
status_code=status_code,
|
|
117
|
+
extra=extra,
|
|
82
118
|
),
|
|
83
119
|
logs=coerce_empty_list_to_none(logs),
|
|
84
120
|
)
|
arcade_core/parse.py
CHANGED
|
@@ -36,6 +36,18 @@ def get_function_name_if_decorated(
|
|
|
36
36
|
and isinstance(decorator.func, ast.Name)
|
|
37
37
|
and decorator.func.id in decorator_ids
|
|
38
38
|
)
|
|
39
|
+
# Support MCPApp tools. e.g., @app.tool or @app.tool(...)
|
|
40
|
+
or (
|
|
41
|
+
isinstance(decorator, ast.Attribute)
|
|
42
|
+
and decorator.attr == "tool"
|
|
43
|
+
and isinstance(decorator.value, ast.Name)
|
|
44
|
+
)
|
|
45
|
+
or (
|
|
46
|
+
isinstance(decorator, ast.Call)
|
|
47
|
+
and isinstance(decorator.func, ast.Attribute)
|
|
48
|
+
and decorator.func.attr == "tool"
|
|
49
|
+
and isinstance(decorator.func.value, ast.Name)
|
|
50
|
+
)
|
|
39
51
|
):
|
|
40
52
|
return node.name
|
|
41
53
|
return None
|
arcade_core/schema.py
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Arcade Core Schema
|
|
3
|
+
|
|
4
|
+
Defines transport-agnostic tool schemas and runtime context protocols used
|
|
5
|
+
across Arcade libraries. This includes:
|
|
6
|
+
|
|
7
|
+
- Tool and toolkit specifications (parameters, outputs, requirements)
|
|
8
|
+
- Transport-agnostic ToolContext carrying authorization, secrets, metadata
|
|
9
|
+
- Runtime ModelContext Protocol and its namespaced sub-protocols for logs,
|
|
10
|
+
progress, resources, tools, prompts, sampling, UI, and notifications
|
|
11
|
+
|
|
12
|
+
Note: ToolContext does not embed runtime capabilities; those are provided by
|
|
13
|
+
implementations of ModelContext (e.g., in arcade-mcp-server) that subclasses ToolContext
|
|
14
|
+
to expose the namespaced APIs to tools without changing function signatures.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
1
19
|
import os
|
|
2
20
|
from dataclasses import dataclass
|
|
3
21
|
from enum import Enum
|
|
@@ -5,6 +23,8 @@ from typing import Any, Literal
|
|
|
5
23
|
|
|
6
24
|
from pydantic import BaseModel, Field
|
|
7
25
|
|
|
26
|
+
from arcade_core.errors import ErrorKind
|
|
27
|
+
|
|
8
28
|
# allow for custom tool name separator
|
|
9
29
|
TOOL_NAME_SEPARATOR = os.getenv("ARCADE_TOOL_NAME_SEPARATOR", ".")
|
|
10
30
|
|
|
@@ -21,10 +41,10 @@ class ValueSchema(BaseModel):
|
|
|
21
41
|
enum: list[str] | None = None
|
|
22
42
|
"""The list of possible values for the value, if it is a closed list."""
|
|
23
43
|
|
|
24
|
-
properties: dict[str,
|
|
44
|
+
properties: dict[str, ValueSchema] | None = None
|
|
25
45
|
"""For object types (json), the schema of nested properties."""
|
|
26
46
|
|
|
27
|
-
inner_properties: dict[str,
|
|
47
|
+
inner_properties: dict[str, ValueSchema] | None = None
|
|
28
48
|
"""For array types with json items, the schema of properties for each array item."""
|
|
29
49
|
|
|
30
50
|
description: str | None = None
|
|
@@ -98,7 +118,7 @@ class ToolAuthRequirement(BaseModel):
|
|
|
98
118
|
# or
|
|
99
119
|
# client.auth.authorize(provider=AuthProvider.google, scopes=["profile", "email"])
|
|
100
120
|
#
|
|
101
|
-
# The Arcade
|
|
121
|
+
# The Arcade TDK translates these into the appropriate provider ID (Google) and type (OAuth2).
|
|
102
122
|
# The only time the developer will set these is if they are using a custom auth provider.
|
|
103
123
|
provider_id: str | None = None
|
|
104
124
|
"""The provider ID configured in Arcade that acts as an alias to well-known configuration."""
|
|
@@ -198,7 +218,7 @@ class FullyQualifiedName:
|
|
|
198
218
|
(self.toolkit_version or "").lower(),
|
|
199
219
|
))
|
|
200
220
|
|
|
201
|
-
def equals_ignoring_version(self, other:
|
|
221
|
+
def equals_ignoring_version(self, other: FullyQualifiedName) -> bool:
|
|
202
222
|
"""Check if two fully-qualified tool names are equal, ignoring the version."""
|
|
203
223
|
return (
|
|
204
224
|
self.name.lower() == other.name.lower()
|
|
@@ -206,7 +226,7 @@ class FullyQualifiedName:
|
|
|
206
226
|
)
|
|
207
227
|
|
|
208
228
|
@staticmethod
|
|
209
|
-
def from_toolkit(tool_name: str, toolkit: ToolkitDefinition) ->
|
|
229
|
+
def from_toolkit(tool_name: str, toolkit: ToolkitDefinition) -> FullyQualifiedName:
|
|
210
230
|
"""Creates a fully-qualified tool name from a tool name and a ToolkitDefinition."""
|
|
211
231
|
return FullyQualifiedName(tool_name, toolkit.name, toolkit.version)
|
|
212
232
|
|
|
@@ -296,7 +316,16 @@ class ToolMetadataItem(BaseModel):
|
|
|
296
316
|
|
|
297
317
|
|
|
298
318
|
class ToolContext(BaseModel):
|
|
299
|
-
"""The context for a tool invocation.
|
|
319
|
+
"""The context for a tool invocation.
|
|
320
|
+
|
|
321
|
+
This type is transport-agnostic and contains only authorization,
|
|
322
|
+
secret, and metadata information needed by the tool. Runtime-specific
|
|
323
|
+
capabilities (logging, resources, etc.) are provided by a separate
|
|
324
|
+
runtime context that wraps this object.
|
|
325
|
+
|
|
326
|
+
Recommendation: For new tools, annotate the parameter as
|
|
327
|
+
`arcade_mcp_server.Context` to access namespaced runtime APIs directly.
|
|
328
|
+
"""
|
|
300
329
|
|
|
301
330
|
authorization: ToolAuthorizationContext | None = None
|
|
302
331
|
"""The authorization context for the tool invocation that requires authorization."""
|
|
@@ -310,16 +339,35 @@ class ToolContext(BaseModel):
|
|
|
310
339
|
user_id: str | None = None
|
|
311
340
|
"""The user ID for the tool invocation (if any)."""
|
|
312
341
|
|
|
342
|
+
model_config = {"arbitrary_types_allowed": True}
|
|
343
|
+
|
|
344
|
+
def set_secret(self, key: str, value: str) -> None:
|
|
345
|
+
"""Add or update a secret to the tool context."""
|
|
346
|
+
if self.secrets is None:
|
|
347
|
+
self.secrets = []
|
|
348
|
+
# Update existing or add new
|
|
349
|
+
for secret in self.secrets:
|
|
350
|
+
if secret.key == key:
|
|
351
|
+
secret.value = value
|
|
352
|
+
return
|
|
353
|
+
self.secrets.append(ToolSecretItem(key=key, value=value))
|
|
354
|
+
|
|
313
355
|
def get_auth_token_or_empty(self) -> str:
|
|
314
356
|
"""Retrieve the authorization token, or return an empty string if not available."""
|
|
315
357
|
return self.authorization.token if self.authorization and self.authorization.token else ""
|
|
316
358
|
|
|
317
359
|
def get_secret(self, key: str) -> str:
|
|
318
|
-
"""Retrieve the secret for the tool invocation.
|
|
360
|
+
"""Retrieve the secret for the tool invocation.
|
|
361
|
+
|
|
362
|
+
Raises a ValueError if the secret is not found.
|
|
363
|
+
"""
|
|
319
364
|
return self._get_item(key, self.secrets, "secret")
|
|
320
365
|
|
|
321
366
|
def get_metadata(self, key: str) -> str:
|
|
322
|
-
"""Retrieve the metadata for the tool invocation.
|
|
367
|
+
"""Retrieve the metadata for the tool invocation.
|
|
368
|
+
|
|
369
|
+
Raises a ValueError if the metadata is not found.
|
|
370
|
+
"""
|
|
323
371
|
return self._get_item(key, self.metadata, "metadata")
|
|
324
372
|
|
|
325
373
|
def _get_item(
|
|
@@ -333,21 +381,14 @@ class ToolContext(BaseModel):
|
|
|
333
381
|
f"{item_name.capitalize()} key passed to get_{item_name} cannot be empty."
|
|
334
382
|
)
|
|
335
383
|
if not items:
|
|
336
|
-
raise ValueError(f"{item_name.capitalize()}
|
|
384
|
+
raise ValueError(f"{item_name.capitalize()} '{key}' not found in context.")
|
|
337
385
|
|
|
338
386
|
normalized_key = key.lower()
|
|
339
387
|
for item in items:
|
|
340
388
|
if item.key.lower() == normalized_key:
|
|
341
389
|
return item.value
|
|
342
390
|
|
|
343
|
-
raise ValueError(f"{item_name.capitalize()} {key} not found in context.")
|
|
344
|
-
|
|
345
|
-
def set_secret(self, key: str, value: str) -> None:
|
|
346
|
-
"""Set a secret for the tool invocation."""
|
|
347
|
-
if not self.secrets:
|
|
348
|
-
self.secrets = []
|
|
349
|
-
secret = ToolSecretItem(key=str(key), value=str(value))
|
|
350
|
-
self.secrets.append(secret)
|
|
391
|
+
raise ValueError(f"{item_name.capitalize()} '{key}' not found in context.")
|
|
351
392
|
|
|
352
393
|
|
|
353
394
|
class ToolCallRequest(BaseModel):
|
|
@@ -390,6 +431,8 @@ class ToolCallError(BaseModel):
|
|
|
390
431
|
|
|
391
432
|
message: str
|
|
392
433
|
"""The user-facing error message."""
|
|
434
|
+
kind: ErrorKind
|
|
435
|
+
"""The error kind that uniquely identifies the kind of error."""
|
|
393
436
|
developer_message: str | None = None
|
|
394
437
|
"""The developer-facing error details."""
|
|
395
438
|
can_retry: bool = False
|
|
@@ -398,8 +441,27 @@ class ToolCallError(BaseModel):
|
|
|
398
441
|
"""Additional content to be included in the retry prompt."""
|
|
399
442
|
retry_after_ms: int | None = None
|
|
400
443
|
"""The number of milliseconds (if any) to wait before retrying the tool call."""
|
|
401
|
-
|
|
402
|
-
"""The
|
|
444
|
+
stacktrace: str | None = None
|
|
445
|
+
"""The stacktrace information for the tool call."""
|
|
446
|
+
status_code: int | None = None
|
|
447
|
+
"""The HTTP status code of the error."""
|
|
448
|
+
extra: dict[str, Any] | None = None
|
|
449
|
+
"""Additional information about the error."""
|
|
450
|
+
|
|
451
|
+
@property
|
|
452
|
+
def is_toolkit_error(self) -> bool:
|
|
453
|
+
"""Check if this error originated from loading a toolkit."""
|
|
454
|
+
return self.kind.name.startswith("TOOLKIT_")
|
|
455
|
+
|
|
456
|
+
@property
|
|
457
|
+
def is_tool_error(self) -> bool:
|
|
458
|
+
"""Check if this error originated from a tool."""
|
|
459
|
+
return self.kind.name.startswith("TOOL_")
|
|
460
|
+
|
|
461
|
+
@property
|
|
462
|
+
def is_upstream_error(self) -> bool:
|
|
463
|
+
"""Check if this error originated from an upstream service."""
|
|
464
|
+
return self.kind.name.startswith("UPSTREAM_")
|
|
403
465
|
|
|
404
466
|
|
|
405
467
|
class ToolCallRequiresAuthorization(BaseModel):
|
|
@@ -418,7 +480,7 @@ class ToolCallRequiresAuthorization(BaseModel):
|
|
|
418
480
|
class ToolCallOutput(BaseModel):
|
|
419
481
|
"""The output of a tool invocation."""
|
|
420
482
|
|
|
421
|
-
value: str | int | float | bool | dict | list
|
|
483
|
+
value: str | int | float | bool | dict | list | None = None
|
|
422
484
|
"""The value returned by the tool."""
|
|
423
485
|
logs: list[ToolCallLog] | None = None
|
|
424
486
|
"""The logs that occurred during the tool invocation."""
|
arcade_core/toolkit.py
CHANGED
|
@@ -6,6 +6,7 @@ import types
|
|
|
6
6
|
from collections import defaultdict
|
|
7
7
|
from pathlib import Path, PurePosixPath, PureWindowsPath
|
|
8
8
|
|
|
9
|
+
import toml
|
|
9
10
|
from pydantic import BaseModel, ConfigDict, field_validator
|
|
10
11
|
|
|
11
12
|
from arcade_core.errors import ToolkitLoadError
|
|
@@ -59,6 +60,71 @@ class Toolkit(BaseModel):
|
|
|
59
60
|
"""
|
|
60
61
|
return cls.from_package(module.__name__)
|
|
61
62
|
|
|
63
|
+
@classmethod
|
|
64
|
+
def from_directory(cls, directory: Path) -> "Toolkit":
|
|
65
|
+
"""
|
|
66
|
+
Load a Toolkit from a directory.
|
|
67
|
+
"""
|
|
68
|
+
pyproject_path = directory / "pyproject.toml"
|
|
69
|
+
if not pyproject_path.exists():
|
|
70
|
+
raise ToolkitLoadError(f"pyproject.toml not found in {directory}")
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
with open(pyproject_path) as f:
|
|
74
|
+
pyproject_data = toml.load(f)
|
|
75
|
+
|
|
76
|
+
project_data = pyproject_data.get("project", {})
|
|
77
|
+
name = project_data.get("name")
|
|
78
|
+
if not name:
|
|
79
|
+
|
|
80
|
+
def _missing_name_error() -> ToolkitLoadError:
|
|
81
|
+
return ToolkitLoadError("name not found in pyproject.toml")
|
|
82
|
+
|
|
83
|
+
raise _missing_name_error() # noqa: TRY301
|
|
84
|
+
|
|
85
|
+
package_name = name
|
|
86
|
+
version = project_data.get("version", "0.0.0")
|
|
87
|
+
description = project_data.get("description", "")
|
|
88
|
+
authors = project_data.get("authors", [])
|
|
89
|
+
author_names = [author.get("name", "") for author in authors]
|
|
90
|
+
|
|
91
|
+
# For homepage and repository, you might need to look under project.urls
|
|
92
|
+
urls = project_data.get("urls", {})
|
|
93
|
+
homepage = urls.get("Homepage")
|
|
94
|
+
repo = urls.get("Repository")
|
|
95
|
+
|
|
96
|
+
except Exception as e:
|
|
97
|
+
raise ToolkitLoadError(f"Failed to load metadata from {pyproject_path}: {e}")
|
|
98
|
+
|
|
99
|
+
# Determine the actual package directory (supports src/ layout and flat layout)
|
|
100
|
+
package_dir = directory
|
|
101
|
+
try:
|
|
102
|
+
src_candidate = directory / "src" / package_name
|
|
103
|
+
flat_candidate = directory / package_name
|
|
104
|
+
if src_candidate.is_dir():
|
|
105
|
+
package_dir = src_candidate
|
|
106
|
+
elif flat_candidate.is_dir():
|
|
107
|
+
package_dir = flat_candidate
|
|
108
|
+
else:
|
|
109
|
+
# Fallback to the provided directory; tools_from_directory will de-duplicate prefixes
|
|
110
|
+
package_dir = directory
|
|
111
|
+
except Exception:
|
|
112
|
+
package_dir = directory
|
|
113
|
+
|
|
114
|
+
toolkit = cls(
|
|
115
|
+
name=name,
|
|
116
|
+
package_name=package_name,
|
|
117
|
+
version=version,
|
|
118
|
+
description=description,
|
|
119
|
+
author=author_names,
|
|
120
|
+
homepage=homepage,
|
|
121
|
+
repository=repo,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
toolkit.tools = cls.tools_from_directory(package_dir, package_name)
|
|
125
|
+
|
|
126
|
+
return toolkit
|
|
127
|
+
|
|
62
128
|
@classmethod
|
|
63
129
|
def from_package(cls, package: str) -> "Toolkit":
|
|
64
130
|
"""
|
|
@@ -232,9 +298,14 @@ class Toolkit(BaseModel):
|
|
|
232
298
|
for module_path in modules:
|
|
233
299
|
relative_path = module_path.relative_to(package_dir)
|
|
234
300
|
cls.validate_file(module_path)
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
301
|
+
# Build import path and avoid duplicating the package prefix if it already exists
|
|
302
|
+
relative_parts = relative_path.with_suffix("").parts
|
|
303
|
+
import_path = ".".join(relative_parts)
|
|
304
|
+
if relative_parts and relative_parts[0] == package_name:
|
|
305
|
+
full_import_path = import_path
|
|
306
|
+
else:
|
|
307
|
+
full_import_path = f"{package_name}.{import_path}" if import_path else package_name
|
|
308
|
+
tools[full_import_path] = get_tools_from_file(str(module_path))
|
|
238
309
|
|
|
239
310
|
if not tools:
|
|
240
311
|
raise ToolkitLoadError(f"No tools found in package {package_name}")
|
arcade_core/utils.py
CHANGED
|
@@ -4,6 +4,7 @@ import ast
|
|
|
4
4
|
import inspect
|
|
5
5
|
import re
|
|
6
6
|
from collections.abc import Callable, Iterable
|
|
7
|
+
from textwrap import dedent
|
|
7
8
|
from types import UnionType
|
|
8
9
|
from typing import Any, Literal, TypeVar, Union, get_args, get_origin
|
|
9
10
|
|
|
@@ -75,7 +76,9 @@ def does_function_return_value(func: Callable) -> bool:
|
|
|
75
76
|
if source is None:
|
|
76
77
|
raise ValueError("Source code not found")
|
|
77
78
|
|
|
78
|
-
|
|
79
|
+
# dedent in case the function is an inner function
|
|
80
|
+
dedented_source = dedent(source)
|
|
81
|
+
tree = ast.parse(dedented_source)
|
|
79
82
|
|
|
80
83
|
class ReturnVisitor(ast.NodeVisitor):
|
|
81
84
|
def __init__(self) -> None:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: arcade-core
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.5.0rc1
|
|
4
4
|
Summary: Arcade Core - Core library for Arcade platform
|
|
5
5
|
Author-email: Arcade <dev@arcade.dev>
|
|
6
6
|
License: MIT
|
|
@@ -14,9 +14,6 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.13
|
|
15
15
|
Requires-Python: >=3.10
|
|
16
16
|
Requires-Dist: loguru>=0.7.0
|
|
17
|
-
Requires-Dist: opentelemetry-exporter-otlp-proto-common==1.28.2
|
|
18
|
-
Requires-Dist: opentelemetry-exporter-otlp-proto-http==1.28.2
|
|
19
|
-
Requires-Dist: opentelemetry-instrumentation-fastapi==0.49b2
|
|
20
17
|
Requires-Dist: packaging>=24.1
|
|
21
18
|
Requires-Dist: pydantic>=2.7.0
|
|
22
19
|
Requires-Dist: pyjwt>=2.8.0
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
arcade_core/__init__.py,sha256=1heu3AROAjpistehPzY2H-2nkj_IjQEh-vVlVOCRF1E,88
|
|
2
|
+
arcade_core/annotations.py,sha256=Nst6aejLWXlpTu7GwzWETu1gQCG1XVAUR_qcFbNvyRc,198
|
|
3
|
+
arcade_core/auth.py,sha256=On9sJPOxvHjKBxgKC1yqp7oijF6KYBsG6fG8KUw-9OY,5882
|
|
4
|
+
arcade_core/catalog.py,sha256=QD7PLaKBW8eBaelj3_ax7s_pgKNnOYWEIsMLtqmbtjI,41819
|
|
5
|
+
arcade_core/config.py,sha256=e98XQAkYySGW9T_yrJg54BB8Wuq06GPVHp7xqe2d1vU,572
|
|
6
|
+
arcade_core/config_model.py,sha256=GYO37yKi7ih6EYKPpX1Kl-K1XwM2JyEJguyaJ7j9TY8,4260
|
|
7
|
+
arcade_core/context.py,sha256=J2MgbVznhJC2qarHq3dTL72W4NGYOM1pjXdI_YwgkA4,3316
|
|
8
|
+
arcade_core/discovery.py,sha256=PluKGhNtJ7RYjJuPDMB8LCNinQLKzlqoAtc3dwKb6IA,8397
|
|
9
|
+
arcade_core/errors.py,sha256=fsi7m6TQQSsdSNHl4rBoSN_YH3ZV910gjvBFqB207f4,13326
|
|
10
|
+
arcade_core/executor.py,sha256=aFRqB4OdC4b8JJN3zekx0hOWYmihWHAZqVWVlSFXzE4,4308
|
|
11
|
+
arcade_core/output.py,sha256=CMY1pHlQIR27Beiz2I-Yg1aO-P-pbsEbhBZ1RdYuflc,4040
|
|
12
|
+
arcade_core/parse.py,sha256=arKGKL9C6g__tRfZ4re6IM_wAqr1v3LrOzTOBEDLhDc,2366
|
|
13
|
+
arcade_core/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
+
arcade_core/schema.py,sha256=l1eE4__sliPIMOp70YxQMlEIpiYQWlBUWEUvr9K1wxA,16927
|
|
15
|
+
arcade_core/toolkit.py,sha256=UcZ151pC8zfIFzVMYxaq31H7M0f-2qprU0PkVAzfRtI,13815
|
|
16
|
+
arcade_core/utils.py,sha256=RxVIzURTtZ4nAWYB3FYGngqMMPmBBxf330Ez9eEoXaw,3109
|
|
17
|
+
arcade_core/version.py,sha256=CpXi3jGlx23RvRyU7iytOMZrnspdWw4yofS8lpP1AJU,18
|
|
18
|
+
arcade_core/converters/openai.py,sha256=4efdgTkvdwT44VGStBhdUmzCnoP5dysceIqPVVPG-vk,7408
|
|
19
|
+
arcade_core-2.5.0rc1.dist-info/METADATA,sha256=bGOwNQd_gIRNUKIe6vheQ6bJIYW51EsjvaPVUybWM6o,2373
|
|
20
|
+
arcade_core-2.5.0rc1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
21
|
+
arcade_core-2.5.0rc1.dist-info/RECORD,,
|
arcade_core/telemetry.py
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
import os
|
|
3
|
-
import urllib.parse
|
|
4
|
-
from typing import Optional
|
|
5
|
-
|
|
6
|
-
from fastapi import FastAPI
|
|
7
|
-
from opentelemetry import _logs, trace
|
|
8
|
-
from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
|
|
9
|
-
from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter
|
|
10
|
-
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
|
|
11
|
-
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
|
|
12
|
-
from opentelemetry.metrics import Meter, get_meter_provider, set_meter_provider
|
|
13
|
-
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
|
|
14
|
-
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
|
|
15
|
-
from opentelemetry.sdk.metrics import MeterProvider
|
|
16
|
-
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
|
|
17
|
-
from opentelemetry.sdk.resources import SERVICE_NAME, Resource
|
|
18
|
-
from opentelemetry.sdk.trace import TracerProvider
|
|
19
|
-
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class ShutdownError(Exception):
|
|
23
|
-
pass
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
class OTELHandler:
|
|
27
|
-
def __init__(self, enable: bool = True, log_level: int = logging.INFO):
|
|
28
|
-
self.enable = enable
|
|
29
|
-
self.log_level = log_level
|
|
30
|
-
self._tracer_provider: Optional[TracerProvider] = None
|
|
31
|
-
self._tracer_span_exporter: Optional[OTLPSpanExporter] = None
|
|
32
|
-
self._meter_provider: Optional[MeterProvider] = None
|
|
33
|
-
self._meter_reader: Optional[PeriodicExportingMetricReader] = None
|
|
34
|
-
self._otlp_metric_exporter: Optional[OTLPMetricExporter] = None
|
|
35
|
-
self._logger_provider: Optional[LoggerProvider] = None
|
|
36
|
-
self._log_processor: Optional[BatchLogRecordProcessor] = None
|
|
37
|
-
self.environment = os.environ.get("ARCADE_ENVIRONMENT", "local")
|
|
38
|
-
|
|
39
|
-
def instrument_app(self, app: FastAPI) -> None:
|
|
40
|
-
if self.enable:
|
|
41
|
-
logging.info(
|
|
42
|
-
"🔎 Initializing OpenTelemetry. Use environment variables to configure the connection"
|
|
43
|
-
)
|
|
44
|
-
self.resource = Resource(
|
|
45
|
-
attributes={SERVICE_NAME: "arcade-worker", "environment": self.environment}
|
|
46
|
-
)
|
|
47
|
-
|
|
48
|
-
self._init_tracer()
|
|
49
|
-
self._init_metrics()
|
|
50
|
-
self._init_logging(self.log_level)
|
|
51
|
-
FastAPIInstrumentor().instrument_app(app)
|
|
52
|
-
|
|
53
|
-
def _init_tracer(self) -> None:
|
|
54
|
-
self._tracer_provider = TracerProvider(resource=self.resource)
|
|
55
|
-
trace.set_tracer_provider(self._tracer_provider)
|
|
56
|
-
|
|
57
|
-
# Create an OTLP exporter
|
|
58
|
-
self._tracer_span_exporter = OTLPSpanExporter()
|
|
59
|
-
|
|
60
|
-
try:
|
|
61
|
-
self._tracer_span_exporter.export([trace.get_tracer(__name__).start_span("ping")])
|
|
62
|
-
except Exception as e:
|
|
63
|
-
raise ConnectionError(
|
|
64
|
-
f"Could not connect to OpenTelemetry Tracer endpoint. Check OpenTelemetry configuration or disable: {e}"
|
|
65
|
-
)
|
|
66
|
-
|
|
67
|
-
# Create a batch span processor and add the exporter
|
|
68
|
-
span_processor = BatchSpanProcessor(self._tracer_span_exporter)
|
|
69
|
-
self._tracer_provider.add_span_processor(span_processor)
|
|
70
|
-
|
|
71
|
-
def _init_metrics(self) -> None:
|
|
72
|
-
self._otlp_metric_exporter = OTLPMetricExporter()
|
|
73
|
-
|
|
74
|
-
self._meter_reader = PeriodicExportingMetricReader(self._otlp_metric_exporter)
|
|
75
|
-
|
|
76
|
-
self._meter_provider = MeterProvider(
|
|
77
|
-
metric_readers=[self._meter_reader], resource=self.resource
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
set_meter_provider(self._meter_provider)
|
|
81
|
-
|
|
82
|
-
def get_meter(self) -> Meter:
|
|
83
|
-
return get_meter_provider().get_meter(__name__)
|
|
84
|
-
|
|
85
|
-
def _init_logging(self, log_level: int) -> None:
|
|
86
|
-
otlp_log_exporter = OTLPLogExporter()
|
|
87
|
-
|
|
88
|
-
self._logger_provider = LoggerProvider(resource=self.resource)
|
|
89
|
-
_logs.set_logger_provider(self._logger_provider)
|
|
90
|
-
|
|
91
|
-
# Create a batch span processor and add the exporter
|
|
92
|
-
self._log_processor = BatchLogRecordProcessor(otlp_log_exporter)
|
|
93
|
-
self._logger_provider.add_log_record_processor(self._log_processor)
|
|
94
|
-
|
|
95
|
-
handler = LoggingHandler(level=log_level, logger_provider=self._logger_provider)
|
|
96
|
-
logging.getLogger().addHandler(handler)
|
|
97
|
-
|
|
98
|
-
# Create a filter for urllib3 connection logs related to OpenTelemetry
|
|
99
|
-
class OTELConnectionFilter(logging.Filter):
|
|
100
|
-
def filter(self, record: logging.LogRecord) -> bool:
|
|
101
|
-
# Filter out connection logs to OpenTelemetry endpoints
|
|
102
|
-
parsed_url = urllib.parse.urlparse(
|
|
103
|
-
os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT", "")
|
|
104
|
-
)
|
|
105
|
-
domain = parsed_url.netloc.split(":")[0]
|
|
106
|
-
return not (domain and domain in str(getattr(record, "args", ())))
|
|
107
|
-
|
|
108
|
-
# Apply the filter to the urllib3 logger
|
|
109
|
-
urllib3_logger = logging.getLogger("urllib3.connectionpool")
|
|
110
|
-
urllib3_logger.addFilter(OTELConnectionFilter())
|
|
111
|
-
|
|
112
|
-
def _shutdown_tracer(self) -> None:
|
|
113
|
-
if self._tracer_span_exporter is None:
|
|
114
|
-
raise ShutdownError("Tracer provider not initialized. Failed to shutdown")
|
|
115
|
-
self._tracer_span_exporter.shutdown()
|
|
116
|
-
|
|
117
|
-
def _shutdown_metrics(self) -> None:
|
|
118
|
-
if self._otlp_metric_exporter is None:
|
|
119
|
-
raise ShutdownError("Meter provider not initialized. Failed to shutdown")
|
|
120
|
-
self._otlp_metric_exporter.shutdown()
|
|
121
|
-
|
|
122
|
-
def _shutdown_logging(self) -> None:
|
|
123
|
-
if self._logger_provider is None:
|
|
124
|
-
raise ShutdownError("Log provider not initialized. Failed to shutdown")
|
|
125
|
-
self._logger_provider.shutdown()
|
|
126
|
-
|
|
127
|
-
def shutdown(self) -> None:
|
|
128
|
-
self._shutdown_tracer()
|
|
129
|
-
self._shutdown_metrics()
|
|
130
|
-
self._shutdown_logging()
|