azurefunctions-agents-runtime 0.0.0a2__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.
Files changed (33) hide show
  1. azure_functions_agents/__init__.py +51 -0
  2. azure_functions_agents/_blob_history.py +368 -0
  3. azure_functions_agents/_credential.py +79 -0
  4. azure_functions_agents/_function_tool.py +97 -0
  5. azure_functions_agents/_logger.py +14 -0
  6. azure_functions_agents/app.py +97 -0
  7. azure_functions_agents/client_manager.py +243 -0
  8. azure_functions_agents/config/__init__.py +78 -0
  9. azure_functions_agents/config/env.py +102 -0
  10. azure_functions_agents/config/loader.py +109 -0
  11. azure_functions_agents/config/merge.py +146 -0
  12. azure_functions_agents/config/paths.py +42 -0
  13. azure_functions_agents/config/schema.py +162 -0
  14. azure_functions_agents/config/validation.py +109 -0
  15. azure_functions_agents/discovery/__init__.py +0 -0
  16. azure_functions_agents/discovery/mcp.py +196 -0
  17. azure_functions_agents/discovery/skills.py +102 -0
  18. azure_functions_agents/discovery/tools.py +133 -0
  19. azure_functions_agents/public/index.html +1523 -0
  20. azure_functions_agents/registration/__init__.py +13 -0
  21. azure_functions_agents/registration/_handlers.py +351 -0
  22. azure_functions_agents/registration/_naming.py +84 -0
  23. azure_functions_agents/registration/capabilities.py +70 -0
  24. azure_functions_agents/registration/endpoints.py +373 -0
  25. azure_functions_agents/registration/triggers.py +151 -0
  26. azure_functions_agents/runner.py +611 -0
  27. azure_functions_agents/system_tools/__init__.py +0 -0
  28. azure_functions_agents/system_tools/sandbox.py +357 -0
  29. azurefunctions_agents_runtime-0.0.0a2.dist-info/METADATA +510 -0
  30. azurefunctions_agents_runtime-0.0.0a2.dist-info/RECORD +33 -0
  31. azurefunctions_agents_runtime-0.0.0a2.dist-info/WHEEL +5 -0
  32. azurefunctions_agents_runtime-0.0.0a2.dist-info/licenses/LICENSE.md +21 -0
  33. azurefunctions_agents_runtime-0.0.0a2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,51 @@
1
+ """Azure Functions agent runtime — public API.
2
+
3
+ This package builds Azure Functions apps backed by the Microsoft Agent
4
+ Framework. The most common entry points are:
5
+
6
+ * :func:`create_function_app` — top-level factory used in ``function_app.py``.
7
+ * :func:`run_agent` / :func:`run_agent_stream` — execute prompts directly
8
+ (e.g. from custom code or tests).
9
+ * :class:`ClientManager` — extension point for plugging in alternate chat
10
+ client providers. The default implementation is :class:`MAFClientManager`
11
+ (auto-detects OpenAI, Azure OpenAI, or Foundry from environment variables).
12
+ * :func:`tool` — decorator for registering Python functions from ``tools/*.py``
13
+ as agent tools.
14
+ """
15
+
16
+ from ._function_tool import tool
17
+ from .app import create_function_app
18
+ from .client_manager import (
19
+ ClientManager,
20
+ MAFClientManager,
21
+ get_client_manager,
22
+ set_client_manager,
23
+ shutdown_client_manager,
24
+ )
25
+ from .config.paths import resolve_config_dir, set_app_root
26
+ from .runner import (
27
+ DEFAULT_MODEL,
28
+ DEFAULT_TIMEOUT,
29
+ AgentResult,
30
+ run_agent,
31
+ run_agent_stream,
32
+ )
33
+ from .system_tools.sandbox import create_sandbox_tools
34
+
35
+ __all__ = [
36
+ "DEFAULT_MODEL",
37
+ "DEFAULT_TIMEOUT",
38
+ "AgentResult",
39
+ "ClientManager",
40
+ "MAFClientManager",
41
+ "create_function_app",
42
+ "create_sandbox_tools",
43
+ "get_client_manager",
44
+ "resolve_config_dir",
45
+ "run_agent",
46
+ "run_agent_stream",
47
+ "set_app_root",
48
+ "set_client_manager",
49
+ "shutdown_client_manager",
50
+ "tool",
51
+ ]
@@ -0,0 +1,368 @@
1
+ """Azure Blob Storage-backed :class:`HistoryProvider` for MAF agent sessions.
2
+
3
+ The Microsoft Agent Framework ships :class:`FileHistoryProvider` for local
4
+ disk storage and :class:`InMemoryHistoryProvider` for tests, but does not yet
5
+ provide a blob-backed implementation. This module fills that gap so the
6
+ runtime can persist multi-turn history to the same storage account that
7
+ Azure Functions already requires (``AzureWebJobsStorage``) — no extra
8
+ resources, no file-share mounts, no storage account keys, and true
9
+ multi-instance support.
10
+
11
+ Wire format
12
+ -----------
13
+
14
+ One blob per session, named ``{blob_prefix}{session_id}.jsonl`` inside a
15
+ single container (default: ``azure-functions-agents``). Blobs are
16
+ **Append Blobs**: every call to :meth:`save_messages` appends the JSON Lines
17
+ serialization of just the new messages from the current turn — this matches
18
+ the contract that MAF's :meth:`HistoryProvider.after_run` only ever passes
19
+ the per-turn delta (input + response messages), never the full history.
20
+
21
+ Concurrency
22
+ -----------
23
+
24
+ ``BlobClient.append_block`` is atomic on the server side, so two Function
25
+ instances appending to the same session blob simultaneously cannot interleave
26
+ within a single block. The documented runtime contract is still
27
+ "one active turn per session id" — cross-instance turn ordering is the
28
+ caller's responsibility.
29
+
30
+ Configuration
31
+ -------------
32
+
33
+ The provider accepts either an Azure Storage connection string or an
34
+ identity-based ``(blob_service_url, credential)`` pair. Helpers in this
35
+ module resolve those from the standard Azure Functions
36
+ ``AzureWebJobsStorage`` settings:
37
+
38
+ * ``AzureWebJobsStorage`` — connection string (local dev, Azurite).
39
+ * ``AzureWebJobsStorage__blobServiceUri`` (+ optional
40
+ ``AzureWebJobsStorage__clientId``) — identity-based; uses
41
+ :class:`DefaultAzureCredential`, honoring the user-assigned client id when
42
+ present (matches the Bicep samples in this repo).
43
+ """
44
+
45
+ from __future__ import annotations
46
+
47
+ import asyncio
48
+ import contextlib
49
+ import hashlib
50
+ import json
51
+ import os
52
+ from collections.abc import Mapping, Sequence
53
+ from typing import Any, ClassVar
54
+
55
+ from agent_framework import HistoryProvider, Message
56
+ from azure.core.exceptions import ResourceExistsError, ResourceNotFoundError
57
+
58
+ from ._logger import logger
59
+
60
+ # ---------------------------------------------------------------------------
61
+ # Constants
62
+ # ---------------------------------------------------------------------------
63
+
64
+ DEFAULT_CONTAINER_NAME = "azure-functions-agents"
65
+ DEFAULT_BLOB_PREFIX = "agent-sessions/"
66
+ DEFAULT_SOURCE_ID = "blob_history"
67
+
68
+ _CONN_STRING_ENV = "AzureWebJobsStorage"
69
+ _BLOB_SERVICE_URI_ENV = "AzureWebJobsStorage__blobServiceUri"
70
+ _CLIENT_ID_ENV = "AzureWebJobsStorage__clientId"
71
+ _CONTAINER_ENV = "AZURE_FUNCTIONS_AGENTS_SESSION_CONTAINER"
72
+
73
+
74
+ # ---------------------------------------------------------------------------
75
+ # Process-wide caches
76
+ # ---------------------------------------------------------------------------
77
+
78
+ # The BlobServiceClient is keyed by a non-secret identifier (account URL for
79
+ # identity-based, or a stable hash sentinel for connection strings) so that
80
+ # secrets never live inside dict keys that could leak into reprs / logs.
81
+ _SERVICE_CLIENTS: dict[str, Any] = {}
82
+ _SERVICE_CLIENTS_LOCK = asyncio.Lock()
83
+
84
+ # Container existence check is process-wide: we only need to create it once
85
+ # per (cache_key, container_name).
86
+ _ENSURED_CONTAINERS: set[tuple[str, str]] = set()
87
+ _ENSURED_CONTAINERS_LOCK = asyncio.Lock()
88
+
89
+
90
+ # ---------------------------------------------------------------------------
91
+ # Provider
92
+ # ---------------------------------------------------------------------------
93
+
94
+
95
+ class BlobHistoryProvider(HistoryProvider):
96
+ """Append-blob-backed :class:`HistoryProvider`.
97
+
98
+ Each session is stored as a single Append Blob named
99
+ ``{blob_prefix}{session_id}.jsonl``. Messages are written as JSON Lines —
100
+ one ``Message.to_dict()`` payload per line.
101
+ """
102
+
103
+ DEFAULT_SOURCE_ID: ClassVar[str] = DEFAULT_SOURCE_ID
104
+
105
+ def __init__(
106
+ self,
107
+ *,
108
+ connection_string: str | None = None,
109
+ blob_service_url: str | None = None,
110
+ credential: Any | None = None,
111
+ container_name: str = DEFAULT_CONTAINER_NAME,
112
+ blob_prefix: str = DEFAULT_BLOB_PREFIX,
113
+ source_id: str = DEFAULT_SOURCE_ID,
114
+ load_messages: bool = True,
115
+ store_inputs: bool = True,
116
+ store_context_messages: bool = False,
117
+ store_context_from: set[str] | None = None,
118
+ store_outputs: bool = True,
119
+ skip_excluded: bool = False,
120
+ ) -> None:
121
+ super().__init__(
122
+ source_id=source_id,
123
+ load_messages=load_messages,
124
+ store_inputs=store_inputs,
125
+ store_context_messages=store_context_messages,
126
+ store_context_from=store_context_from,
127
+ store_outputs=store_outputs,
128
+ )
129
+ if not connection_string and not blob_service_url:
130
+ raise ValueError(
131
+ "BlobHistoryProvider requires either 'connection_string' or 'blob_service_url'."
132
+ )
133
+ self.skip_excluded = skip_excluded
134
+ self._connection_string = connection_string
135
+ self._blob_service_url = blob_service_url
136
+ self._credential = credential
137
+ self._container_name = container_name
138
+ self._blob_prefix = _normalize_prefix(blob_prefix)
139
+ self._cache_key = _service_client_cache_key(
140
+ connection_string=connection_string,
141
+ blob_service_url=blob_service_url,
142
+ )
143
+
144
+ # ------------------------------------------------------------------
145
+ # MAF HistoryProvider surface
146
+ # ------------------------------------------------------------------
147
+
148
+ async def get_messages(
149
+ self,
150
+ session_id: str | None,
151
+ *,
152
+ state: dict[str, Any] | None = None,
153
+ **kwargs: Any,
154
+ ) -> list[Message]:
155
+ del state, kwargs
156
+ blob_client = await self._get_blob_client(session_id)
157
+ try:
158
+ downloader = await blob_client.download_blob(encoding="utf-8")
159
+ content = await downloader.readall()
160
+ except ResourceNotFoundError:
161
+ return []
162
+
163
+ text = content if isinstance(content, str) else content.decode("utf-8")
164
+ messages: list[Message] = []
165
+ for line_number, raw in enumerate(text.splitlines(), start=1):
166
+ line = raw.strip()
167
+ if not line:
168
+ continue
169
+ try:
170
+ payload = json.loads(line)
171
+ except ValueError as exc:
172
+ raise ValueError(
173
+ f"Failed to deserialize history line {line_number} from blob "
174
+ f"'{self._container_name}/{self._blob_name(session_id)}'."
175
+ ) from exc
176
+ if not isinstance(payload, Mapping):
177
+ raise ValueError(
178
+ f"History line {line_number} in blob "
179
+ f"'{self._container_name}/{self._blob_name(session_id)}' "
180
+ "did not deserialize to a mapping."
181
+ )
182
+ messages.append(Message.from_dict(dict(payload)))
183
+
184
+ if self.skip_excluded:
185
+ messages = [
186
+ m for m in messages if not m.additional_properties.get("_excluded", False)
187
+ ]
188
+ return messages
189
+
190
+ async def save_messages(
191
+ self,
192
+ session_id: str | None,
193
+ messages: Sequence[Message],
194
+ *,
195
+ state: dict[str, Any] | None = None,
196
+ **kwargs: Any,
197
+ ) -> None:
198
+ del state, kwargs
199
+ if not messages:
200
+ return
201
+
202
+ payload = "".join(f"{_serialize_message(message)}\n" for message in messages)
203
+ data = payload.encode("utf-8")
204
+
205
+ blob_client = await self._get_blob_client(session_id)
206
+ try:
207
+ await blob_client.append_block(data)
208
+ return
209
+ except ResourceNotFoundError:
210
+ pass
211
+
212
+ # Blob does not exist yet — create it and retry. Suppress the
213
+ # "already exists" race that happens when another instance wins
214
+ # the create.
215
+ with contextlib.suppress(ResourceExistsError):
216
+ await blob_client.create_append_blob()
217
+ await blob_client.append_block(data)
218
+
219
+ # ------------------------------------------------------------------
220
+ # Internals
221
+ # ------------------------------------------------------------------
222
+
223
+ def _blob_name(self, session_id: str | None) -> str:
224
+ stem = session_id or "default"
225
+ return f"{self._blob_prefix}{stem}.jsonl"
226
+
227
+ async def _get_blob_client(self, session_id: str | None) -> Any:
228
+ service_client = await self._get_service_client()
229
+ await self._ensure_container(service_client)
230
+ return service_client.get_blob_client(
231
+ container=self._container_name,
232
+ blob=self._blob_name(session_id),
233
+ )
234
+
235
+ async def _get_service_client(self) -> Any:
236
+ cached = _SERVICE_CLIENTS.get(self._cache_key)
237
+ if cached is not None:
238
+ return cached
239
+ async with _SERVICE_CLIENTS_LOCK:
240
+ cached = _SERVICE_CLIENTS.get(self._cache_key)
241
+ if cached is not None:
242
+ return cached
243
+ client = _build_service_client(
244
+ connection_string=self._connection_string,
245
+ blob_service_url=self._blob_service_url,
246
+ credential=self._credential,
247
+ )
248
+ _SERVICE_CLIENTS[self._cache_key] = client
249
+ return client
250
+
251
+ async def _ensure_container(self, service_client: Any) -> None:
252
+ key = (self._cache_key, self._container_name)
253
+ if key in _ENSURED_CONTAINERS:
254
+ return
255
+ async with _ENSURED_CONTAINERS_LOCK:
256
+ if key in _ENSURED_CONTAINERS:
257
+ return
258
+ container_client = service_client.get_container_client(self._container_name)
259
+ with contextlib.suppress(ResourceExistsError):
260
+ await container_client.create_container()
261
+ _ENSURED_CONTAINERS.add(key)
262
+
263
+
264
+ # ---------------------------------------------------------------------------
265
+ # Module-level helpers
266
+ # ---------------------------------------------------------------------------
267
+
268
+
269
+ def _serialize_message(message: Message) -> str:
270
+ payload = message.to_dict()
271
+ serialized = json.dumps(payload)
272
+ if "\n" in serialized or "\r" in serialized:
273
+ raise ValueError("Serialized message must not contain newline characters for JSONL.")
274
+ return serialized
275
+
276
+
277
+ def _normalize_prefix(prefix: str) -> str:
278
+ """Strip leading slashes and ensure exactly one trailing slash if non-empty."""
279
+ cleaned = (prefix or "").lstrip("/")
280
+ if cleaned and not cleaned.endswith("/"):
281
+ cleaned = f"{cleaned}/"
282
+ return cleaned
283
+
284
+
285
+ def _service_client_cache_key(
286
+ *,
287
+ connection_string: str | None,
288
+ blob_service_url: str | None,
289
+ ) -> str:
290
+ """Build a non-secret cache key for the :class:`BlobServiceClient` cache.
291
+
292
+ We intentionally avoid storing the raw connection string as a dict key so
293
+ that secrets do not surface in error reprs or debug dumps. A short hash
294
+ is sufficient because we only need stable equality for the cache.
295
+ """
296
+ if blob_service_url:
297
+ return f"url::{blob_service_url}"
298
+ assert connection_string is not None
299
+ digest = hashlib.sha256(connection_string.encode("utf-8")).hexdigest()[:32]
300
+ return f"conn::{digest}"
301
+
302
+
303
+ def _build_service_client(
304
+ *,
305
+ connection_string: str | None,
306
+ blob_service_url: str | None,
307
+ credential: Any | None,
308
+ ) -> Any:
309
+ from azure.storage.blob.aio import BlobServiceClient
310
+
311
+ if connection_string:
312
+ return BlobServiceClient.from_connection_string(connection_string)
313
+ assert blob_service_url is not None
314
+ if credential is None:
315
+ from ._credential import build_async_credential_with_client_id
316
+
317
+ # Precedence: storage-specific identity (AzureWebJobsStorage__clientId) wins,
318
+ # then app-wide AZURE_CLIENT_ID, then bare DefaultAzureCredential().
319
+ client_id = (
320
+ os.environ.get(_CLIENT_ID_ENV) or os.environ.get("AZURE_CLIENT_ID") or ""
321
+ ).strip()
322
+ credential = build_async_credential_with_client_id(client_id)
323
+ return BlobServiceClient(account_url=blob_service_url, credential=credential)
324
+
325
+
326
+ # ---------------------------------------------------------------------------
327
+ # Factory used by the runner
328
+ # ---------------------------------------------------------------------------
329
+
330
+
331
+ def build_blob_provider_from_environment(
332
+ *,
333
+ container_name: str | None = None,
334
+ ) -> BlobHistoryProvider | None:
335
+ """Construct a :class:`BlobHistoryProvider` from ``AzureWebJobsStorage`` env vars.
336
+
337
+ Returns ``None`` if neither a connection string nor a blob service URI is
338
+ configured. Honors both the connection-string form (used locally with
339
+ Azurite) and the identity-based form
340
+ (``AzureWebJobsStorage__blobServiceUri``) that Azure Functions deploys
341
+ use with managed identity.
342
+ """
343
+ conn = (os.environ.get(_CONN_STRING_ENV) or "").strip()
344
+ uri = (os.environ.get(_BLOB_SERVICE_URI_ENV) or "").strip()
345
+ if not conn and not uri:
346
+ return None
347
+ container = container_name or (os.environ.get(_CONTAINER_ENV) or "").strip() or None
348
+ kwargs: dict[str, Any] = {}
349
+ if container:
350
+ kwargs["container_name"] = container
351
+ if conn:
352
+ logger.info(
353
+ "BlobHistoryProvider: using AzureWebJobsStorage connection string (container=%s).",
354
+ container or DEFAULT_CONTAINER_NAME,
355
+ )
356
+ return BlobHistoryProvider(connection_string=conn, **kwargs)
357
+ logger.info(
358
+ "BlobHistoryProvider: using AzureWebJobsStorage__blobServiceUri=%s (container=%s).",
359
+ uri,
360
+ container or DEFAULT_CONTAINER_NAME,
361
+ )
362
+ return BlobHistoryProvider(blob_service_url=uri, **kwargs)
363
+
364
+
365
+ def reset_caches_for_testing() -> None:
366
+ """Drop the module-level caches. Test-only helper."""
367
+ _SERVICE_CLIENTS.clear()
368
+ _ENSURED_CONTAINERS.clear()
@@ -0,0 +1,79 @@
1
+ """Shared ``DefaultAzureCredential`` builders that honor ``AZURE_CLIENT_ID``.
2
+
3
+ Multi-identity Function Apps need an explicit ``managed_identity_client_id``;
4
+ without one, :class:`DefaultAzureCredential` picks a managed identity
5
+ non-deterministically when more than one is assigned. These helpers centralize
6
+ the ``AZURE_CLIENT_ID`` lookup so every component (the MAF client manager,
7
+ the ACA sandbox, the ARM/connector data-plane clients) selects the same
8
+ identity in the same way.
9
+
10
+ ``BlobHistoryProvider`` deliberately does **not** use these helpers because it
11
+ follows a storage-specific precedence (``AzureWebJobsStorage__clientId`` wins,
12
+ then falls back to ``AZURE_CLIENT_ID``); see
13
+ :mod:`azure_functions_agents._blob_history`.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import os
19
+ from typing import TYPE_CHECKING
20
+
21
+ if TYPE_CHECKING:
22
+ from azure.identity import DefaultAzureCredential as SyncDefaultAzureCredential
23
+ from azure.identity.aio import DefaultAzureCredential as AsyncDefaultAzureCredential
24
+
25
+
26
+ _AZURE_CLIENT_ID_ENV = "AZURE_CLIENT_ID"
27
+
28
+
29
+ def build_credential() -> SyncDefaultAzureCredential:
30
+ """Return a sync ``DefaultAzureCredential`` honoring ``AZURE_CLIENT_ID``."""
31
+ from azure.identity import DefaultAzureCredential
32
+
33
+ client_id = os.environ.get(_AZURE_CLIENT_ID_ENV)
34
+ if client_id:
35
+ return DefaultAzureCredential(managed_identity_client_id=client_id)
36
+ return DefaultAzureCredential()
37
+
38
+
39
+ def build_credential_with_client_id(
40
+ client_id: str | None,
41
+ ) -> SyncDefaultAzureCredential:
42
+ """Return a sync ``DefaultAzureCredential`` for a caller-supplied client id.
43
+
44
+ Pass an empty/``None`` value to fall back to a bare
45
+ :class:`DefaultAzureCredential`.
46
+ """
47
+ from azure.identity import DefaultAzureCredential
48
+
49
+ if client_id:
50
+ return DefaultAzureCredential(managed_identity_client_id=client_id)
51
+ return DefaultAzureCredential()
52
+
53
+
54
+ def build_async_credential() -> AsyncDefaultAzureCredential:
55
+ """Return an async ``DefaultAzureCredential`` honoring ``AZURE_CLIENT_ID``."""
56
+ from azure.identity.aio import DefaultAzureCredential
57
+
58
+ client_id = os.environ.get(_AZURE_CLIENT_ID_ENV)
59
+ if client_id:
60
+ return DefaultAzureCredential(managed_identity_client_id=client_id)
61
+ return DefaultAzureCredential()
62
+
63
+
64
+ def build_async_credential_with_client_id(
65
+ client_id: str | None,
66
+ ) -> AsyncDefaultAzureCredential:
67
+ """Return an async ``DefaultAzureCredential`` for a caller-supplied client id.
68
+
69
+ Use this when the calling module has its own precedence rules for which
70
+ environment variable identifies the managed identity (e.g.
71
+ :class:`BlobHistoryProvider` prefers ``AzureWebJobsStorage__clientId`` and
72
+ only falls back to ``AZURE_CLIENT_ID``). Pass an empty/``None`` value to
73
+ fall back to a bare :class:`DefaultAzureCredential`.
74
+ """
75
+ from azure.identity.aio import DefaultAzureCredential
76
+
77
+ if client_id:
78
+ return DefaultAzureCredential(managed_identity_client_id=client_id)
79
+ return DefaultAzureCredential()
@@ -0,0 +1,97 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ from collections.abc import Awaitable, Callable
5
+ from functools import wraps
6
+ from typing import Any, TypeVar, overload
7
+
8
+ from agent_framework import FunctionTool
9
+ from pydantic import BaseModel
10
+
11
+ __all__ = ["FunctionTool", "tool"]
12
+
13
+ SchemaT = TypeVar("SchemaT", bound=BaseModel)
14
+
15
+
16
+ def _wrap_with_schema( # noqa: UP047
17
+ func: Callable[[SchemaT], Any],
18
+ schema: type[SchemaT],
19
+ ) -> Callable[..., Awaitable[Any]]:
20
+ @wraps(func)
21
+ async def wrapper(**kwargs: Any) -> Any:
22
+ params = schema(**kwargs)
23
+ result = func(params)
24
+ if inspect.isawaitable(result):
25
+ return await result
26
+ return result
27
+
28
+ return wrapper
29
+
30
+
31
+ @overload
32
+ def tool(
33
+ func: Callable[..., Any],
34
+ *,
35
+ name: str | None = None,
36
+ description: str | None = None,
37
+ schema: None = None,
38
+ **kwargs: Any,
39
+ ) -> FunctionTool: ...
40
+
41
+
42
+ @overload
43
+ def tool( # noqa: UP047
44
+ func: Callable[[SchemaT], Any],
45
+ *,
46
+ name: str | None = None,
47
+ description: str | None = None,
48
+ schema: type[SchemaT],
49
+ **kwargs: Any,
50
+ ) -> FunctionTool: ...
51
+
52
+
53
+ @overload
54
+ def tool(
55
+ *,
56
+ name: str | None = None,
57
+ description: str | None = None,
58
+ schema: None = None,
59
+ **kwargs: Any,
60
+ ) -> Callable[[Callable[..., Any]], FunctionTool]: ...
61
+
62
+
63
+ @overload
64
+ def tool( # noqa: UP047
65
+ *,
66
+ name: str | None = None,
67
+ description: str | None = None,
68
+ schema: type[SchemaT],
69
+ **kwargs: Any,
70
+ ) -> Callable[[Callable[[SchemaT], Any]], FunctionTool]: ...
71
+
72
+
73
+ def tool(
74
+ func: Callable[..., Any] | None = None,
75
+ *,
76
+ name: str | None = None,
77
+ description: str | None = None,
78
+ schema: type[BaseModel] | None = None,
79
+ **kwargs: Any,
80
+ ) -> FunctionTool | Callable[[Callable[..., Any]], FunctionTool]:
81
+ def decorator(inner: Callable[..., Any]) -> FunctionTool:
82
+ wrapped: Callable[..., Any] = inner
83
+ input_model: type[BaseModel] | None = None
84
+ if schema is not None:
85
+ wrapped = _wrap_with_schema(inner, schema)
86
+ input_model = schema
87
+ return FunctionTool(
88
+ name=name or inner.__name__,
89
+ description=(description or inner.__doc__ or "").strip(),
90
+ func=wrapped,
91
+ input_model=input_model,
92
+ **kwargs,
93
+ )
94
+
95
+ if func is not None:
96
+ return decorator(func)
97
+ return decorator
@@ -0,0 +1,14 @@
1
+ """Shared logger for the Azure Functions Agent Runtime.
2
+
3
+ All modules in this package use this single logger so that operators can
4
+ filter or configure the entire runtime's output in one place.
5
+
6
+ In Application Insights the ``customDimensions.Category`` field will be
7
+ ``azure.functions.AgentRuntime`` — consistent with the ``azure.functions.*``
8
+ naming convention used by the Azure Functions Python SDK (e.g.
9
+ ``azure.functions.AsgiMiddleware``).
10
+ """
11
+
12
+ import logging
13
+
14
+ logger = logging.getLogger("azure.functions.AgentRuntime")