mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: Add initial scaffold for durabletask package (#2761)
* Add initial scaffold * Update design * Fix mypy and update design * add additional style considered * Address comments * Fix test * Update readmes
This commit is contained in:
committed by
GitHub
Unverified
parent
638fbb5f03
commit
a48a8dd524
@@ -0,0 +1,83 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
"""Durable Task integration for Microsoft Agent Framework."""
|
||||
|
||||
from ._callbacks import AgentCallbackContext, AgentResponseCallbackProtocol
|
||||
from ._constants import (
|
||||
DEFAULT_MAX_POLL_RETRIES,
|
||||
DEFAULT_POLL_INTERVAL_SECONDS,
|
||||
MIMETYPE_APPLICATION_JSON,
|
||||
MIMETYPE_TEXT_PLAIN,
|
||||
REQUEST_RESPONSE_FORMAT_JSON,
|
||||
REQUEST_RESPONSE_FORMAT_TEXT,
|
||||
THREAD_ID_FIELD,
|
||||
THREAD_ID_HEADER,
|
||||
WAIT_FOR_RESPONSE_FIELD,
|
||||
WAIT_FOR_RESPONSE_HEADER,
|
||||
ApiResponseFields,
|
||||
ContentTypes,
|
||||
DurableStateFields,
|
||||
)
|
||||
from ._durable_agent_state import (
|
||||
DurableAgentState,
|
||||
DurableAgentStateContent,
|
||||
DurableAgentStateData,
|
||||
DurableAgentStateDataContent,
|
||||
DurableAgentStateEntry,
|
||||
DurableAgentStateEntryJsonType,
|
||||
DurableAgentStateErrorContent,
|
||||
DurableAgentStateFunctionCallContent,
|
||||
DurableAgentStateFunctionResultContent,
|
||||
DurableAgentStateHostedFileContent,
|
||||
DurableAgentStateHostedVectorStoreContent,
|
||||
DurableAgentStateMessage,
|
||||
DurableAgentStateRequest,
|
||||
DurableAgentStateResponse,
|
||||
DurableAgentStateTextContent,
|
||||
DurableAgentStateTextReasoningContent,
|
||||
DurableAgentStateUnknownContent,
|
||||
DurableAgentStateUriContent,
|
||||
DurableAgentStateUsage,
|
||||
DurableAgentStateUsageContent,
|
||||
)
|
||||
from ._models import RunRequest, serialize_response_format
|
||||
|
||||
__all__ = [
|
||||
"DEFAULT_MAX_POLL_RETRIES",
|
||||
"DEFAULT_POLL_INTERVAL_SECONDS",
|
||||
"MIMETYPE_APPLICATION_JSON",
|
||||
"MIMETYPE_TEXT_PLAIN",
|
||||
"REQUEST_RESPONSE_FORMAT_JSON",
|
||||
"REQUEST_RESPONSE_FORMAT_TEXT",
|
||||
"THREAD_ID_FIELD",
|
||||
"THREAD_ID_HEADER",
|
||||
"WAIT_FOR_RESPONSE_FIELD",
|
||||
"WAIT_FOR_RESPONSE_HEADER",
|
||||
"AgentCallbackContext",
|
||||
"AgentResponseCallbackProtocol",
|
||||
"ApiResponseFields",
|
||||
"ContentTypes",
|
||||
"DurableAgentState",
|
||||
"DurableAgentStateContent",
|
||||
"DurableAgentStateData",
|
||||
"DurableAgentStateDataContent",
|
||||
"DurableAgentStateEntry",
|
||||
"DurableAgentStateEntryJsonType",
|
||||
"DurableAgentStateErrorContent",
|
||||
"DurableAgentStateFunctionCallContent",
|
||||
"DurableAgentStateFunctionResultContent",
|
||||
"DurableAgentStateHostedFileContent",
|
||||
"DurableAgentStateHostedVectorStoreContent",
|
||||
"DurableAgentStateMessage",
|
||||
"DurableAgentStateRequest",
|
||||
"DurableAgentStateResponse",
|
||||
"DurableAgentStateTextContent",
|
||||
"DurableAgentStateTextReasoningContent",
|
||||
"DurableAgentStateUnknownContent",
|
||||
"DurableAgentStateUriContent",
|
||||
"DurableAgentStateUsage",
|
||||
"DurableAgentStateUsageContent",
|
||||
"DurableStateFields",
|
||||
"RunRequest",
|
||||
"serialize_response_format",
|
||||
]
|
||||
@@ -0,0 +1,40 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
"""Callback interfaces for Durable Agent executions.
|
||||
|
||||
This module enables callers of AgentFunctionApp to supply streaming and final-response callbacks that are
|
||||
invoked during durable entity execution.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Protocol
|
||||
|
||||
from agent_framework import AgentRunResponse, AgentRunResponseUpdate
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AgentCallbackContext:
|
||||
"""Context supplied to callback invocations."""
|
||||
|
||||
agent_name: str
|
||||
correlation_id: str
|
||||
thread_id: str | None = None
|
||||
request_message: str | None = None
|
||||
|
||||
|
||||
class AgentResponseCallbackProtocol(Protocol):
|
||||
"""Protocol describing the callbacks invoked during agent execution."""
|
||||
|
||||
async def on_streaming_response_update(
|
||||
self,
|
||||
update: AgentRunResponseUpdate,
|
||||
context: AgentCallbackContext,
|
||||
) -> None:
|
||||
"""Handle a streaming response update emitted by the agent."""
|
||||
|
||||
async def on_agent_response(
|
||||
self,
|
||||
response: AgentRunResponse,
|
||||
context: AgentCallbackContext,
|
||||
) -> None:
|
||||
"""Handle the final agent response."""
|
||||
@@ -0,0 +1,130 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
"""Constants for Azure Functions Agent Framework integration.
|
||||
|
||||
This module contains:
|
||||
- Runtime configuration constants (polling, MIME types, headers)
|
||||
- JSON field name mappings for camelCase (JSON) ↔ snake_case (Python) serialization
|
||||
|
||||
For serialization constants, use the DurableStateFields, ContentTypes, and EntryTypes classes
|
||||
to ensure consistent field naming between to_dict() and from_dict() methods.
|
||||
"""
|
||||
|
||||
from typing import Final
|
||||
|
||||
# Supported request/response formats and MIME types
|
||||
REQUEST_RESPONSE_FORMAT_JSON: str = "json"
|
||||
REQUEST_RESPONSE_FORMAT_TEXT: str = "text"
|
||||
MIMETYPE_APPLICATION_JSON: str = "application/json"
|
||||
MIMETYPE_TEXT_PLAIN: str = "text/plain"
|
||||
|
||||
# Field and header names
|
||||
THREAD_ID_FIELD: str = "thread_id"
|
||||
THREAD_ID_HEADER: str = "x-ms-thread-id"
|
||||
WAIT_FOR_RESPONSE_FIELD: str = "wait_for_response"
|
||||
WAIT_FOR_RESPONSE_HEADER: str = "x-ms-wait-for-response"
|
||||
|
||||
# Polling configuration
|
||||
DEFAULT_MAX_POLL_RETRIES: int = 30
|
||||
DEFAULT_POLL_INTERVAL_SECONDS: float = 1.0
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# JSON Field Name Constants for Durable Agent State Serialization
|
||||
# =============================================================================
|
||||
# These constants ensure consistent camelCase field names in JSON serialization.
|
||||
# Use these in both to_dict() and from_dict() methods to prevent mismatches.
|
||||
|
||||
# NOTE: Changing these constants is a breaking change and might require a schema version bump.
|
||||
|
||||
|
||||
class DurableStateFields:
|
||||
"""JSON field name constants for durable agent state serialization.
|
||||
|
||||
All field names are in camelCase to match the JSON schema.
|
||||
Use these constants in both to_dict() and from_dict() methods.
|
||||
"""
|
||||
|
||||
# Schema-level fields
|
||||
SCHEMA_VERSION: Final[str] = "schemaVersion"
|
||||
DATA: Final[str] = "data"
|
||||
|
||||
# Entry discriminator
|
||||
TYPE_DISCRIMINATOR: Final[str] = "$type"
|
||||
|
||||
# Internal field names
|
||||
JSON_TYPE: Final[str] = "json_type"
|
||||
TYPE_INTERNAL: Final[str] = "type"
|
||||
|
||||
# Common entry fields
|
||||
CORRELATION_ID: Final[str] = "correlationId"
|
||||
CREATED_AT: Final[str] = "createdAt"
|
||||
MESSAGES: Final[str] = "messages"
|
||||
EXTENSION_DATA: Final[str] = "extensionData"
|
||||
|
||||
# Request-specific fields
|
||||
RESPONSE_TYPE: Final[str] = "responseType"
|
||||
RESPONSE_SCHEMA: Final[str] = "responseSchema"
|
||||
ORCHESTRATION_ID: Final[str] = "orchestrationId"
|
||||
|
||||
# Response-specific fields
|
||||
USAGE: Final[str] = "usage"
|
||||
|
||||
# Message fields
|
||||
ROLE: Final[str] = "role"
|
||||
CONTENTS: Final[str] = "contents"
|
||||
AUTHOR_NAME: Final[str] = "authorName"
|
||||
|
||||
# Content fields
|
||||
TEXT: Final[str] = "text"
|
||||
URI: Final[str] = "uri"
|
||||
MEDIA_TYPE: Final[str] = "mediaType"
|
||||
MESSAGE: Final[str] = "message"
|
||||
ERROR_CODE: Final[str] = "errorCode"
|
||||
DETAILS: Final[str] = "details"
|
||||
CALL_ID: Final[str] = "callId"
|
||||
NAME: Final[str] = "name"
|
||||
ARGUMENTS: Final[str] = "arguments"
|
||||
RESULT: Final[str] = "result"
|
||||
FILE_ID: Final[str] = "fileId"
|
||||
VECTOR_STORE_ID: Final[str] = "vectorStoreId"
|
||||
CONTENT: Final[str] = "content"
|
||||
|
||||
# Usage fields (noqa: S105 - these are JSON field names, not passwords)
|
||||
INPUT_TOKEN_COUNT: Final[str] = "inputTokenCount" # noqa: S105
|
||||
OUTPUT_TOKEN_COUNT: Final[str] = "outputTokenCount" # noqa: S105
|
||||
TOTAL_TOKEN_COUNT: Final[str] = "totalTokenCount" # noqa: S105
|
||||
|
||||
# History field
|
||||
CONVERSATION_HISTORY: Final[str] = "conversationHistory"
|
||||
|
||||
|
||||
class ContentTypes:
|
||||
"""Content type discriminator values for the $type field.
|
||||
|
||||
These values are used in the JSON $type field to identify content types.
|
||||
"""
|
||||
|
||||
TEXT: Final[str] = "text"
|
||||
DATA: Final[str] = "data"
|
||||
ERROR: Final[str] = "error"
|
||||
FUNCTION_CALL: Final[str] = "functionCall"
|
||||
FUNCTION_RESULT: Final[str] = "functionResult"
|
||||
HOSTED_FILE: Final[str] = "hostedFile"
|
||||
HOSTED_VECTOR_STORE: Final[str] = "hostedVectorStore"
|
||||
REASONING: Final[str] = "reasoning"
|
||||
URI: Final[str] = "uri"
|
||||
USAGE: Final[str] = "usage"
|
||||
UNKNOWN: Final[str] = "unknown"
|
||||
|
||||
|
||||
class ApiResponseFields:
|
||||
"""Field names for HTTP API responses (not part of persisted schema).
|
||||
|
||||
These are used in try_get_agent_response() for backward compatibility
|
||||
with the HTTP API response format.
|
||||
"""
|
||||
|
||||
CONTENT: Final[str] = "content"
|
||||
MESSAGE_COUNT: Final[str] = "message_count"
|
||||
CORRELATION_ID: Final[str] = "correlationId"
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,195 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
"""Data models for Durable Agent Framework.
|
||||
|
||||
This module defines the request and response models used by the framework.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from importlib import import_module
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
from agent_framework import Role
|
||||
|
||||
from ._constants import REQUEST_RESPONSE_FORMAT_TEXT
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover - type checking imports only
|
||||
from pydantic import BaseModel
|
||||
|
||||
_PydanticBaseModel: type[BaseModel] | None
|
||||
|
||||
try:
|
||||
from pydantic import BaseModel as _RuntimeBaseModel
|
||||
except ImportError: # pragma: no cover - optional dependency
|
||||
_PydanticBaseModel = None
|
||||
else:
|
||||
_PydanticBaseModel = _RuntimeBaseModel
|
||||
|
||||
|
||||
def serialize_response_format(response_format: type[BaseModel] | None) -> Any:
|
||||
"""Serialize response format for transport across durable function boundaries."""
|
||||
if response_format is None:
|
||||
return None
|
||||
|
||||
if _PydanticBaseModel is None:
|
||||
raise RuntimeError("pydantic is required to use structured response formats")
|
||||
|
||||
if not inspect.isclass(response_format) or not issubclass(response_format, _PydanticBaseModel):
|
||||
raise TypeError("response_format must be a Pydantic BaseModel type")
|
||||
|
||||
return {
|
||||
"__response_schema_type__": "pydantic_model",
|
||||
"module": response_format.__module__,
|
||||
"qualname": response_format.__qualname__,
|
||||
}
|
||||
|
||||
|
||||
def _deserialize_response_format(response_format: Any) -> type[BaseModel] | None:
|
||||
"""Deserialize response format back into actionable type if possible."""
|
||||
if response_format is None:
|
||||
return None
|
||||
|
||||
if (
|
||||
_PydanticBaseModel is not None
|
||||
and inspect.isclass(response_format)
|
||||
and issubclass(response_format, _PydanticBaseModel)
|
||||
):
|
||||
return response_format
|
||||
|
||||
if not isinstance(response_format, dict):
|
||||
return None
|
||||
|
||||
response_dict = cast(dict[str, Any], response_format)
|
||||
|
||||
if response_dict.get("__response_schema_type__") != "pydantic_model":
|
||||
return None
|
||||
|
||||
module_name = response_dict.get("module")
|
||||
qualname = response_dict.get("qualname")
|
||||
if not module_name or not qualname:
|
||||
return None
|
||||
|
||||
try:
|
||||
module = import_module(module_name)
|
||||
except ImportError: # pragma: no cover - user provided module missing
|
||||
return None
|
||||
|
||||
attr: Any = module
|
||||
for part in qualname.split("."):
|
||||
try:
|
||||
attr = getattr(attr, part)
|
||||
except AttributeError: # pragma: no cover - invalid qualname
|
||||
return None
|
||||
|
||||
if _PydanticBaseModel is not None and inspect.isclass(attr) and issubclass(attr, _PydanticBaseModel):
|
||||
return attr
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@dataclass
|
||||
class RunRequest:
|
||||
"""Represents a request to run an agent with a specific message and configuration.
|
||||
|
||||
Attributes:
|
||||
message: The message to send to the agent
|
||||
request_response_format: The desired response format (e.g., "text" or "json")
|
||||
role: The role of the message sender (user, system, or assistant)
|
||||
response_format: Optional Pydantic BaseModel type describing the structured response format
|
||||
enable_tool_calls: Whether to enable tool calls for this request
|
||||
thread_id: Optional thread ID for tracking
|
||||
correlation_id: Optional correlation ID for tracking the response to this specific request
|
||||
created_at: Optional timestamp when the request was created
|
||||
orchestration_id: Optional ID of the orchestration that initiated this request
|
||||
"""
|
||||
|
||||
message: str
|
||||
request_response_format: str
|
||||
role: Role = Role.USER
|
||||
response_format: type[BaseModel] | None = None
|
||||
enable_tool_calls: bool = True
|
||||
thread_id: str | None = None
|
||||
correlation_id: str | None = None
|
||||
created_at: datetime | None = None
|
||||
orchestration_id: str | None = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: str,
|
||||
request_response_format: str = REQUEST_RESPONSE_FORMAT_TEXT,
|
||||
role: Role | str | None = Role.USER,
|
||||
response_format: type[BaseModel] | None = None,
|
||||
enable_tool_calls: bool = True,
|
||||
thread_id: str | None = None,
|
||||
correlation_id: str | None = None,
|
||||
created_at: datetime | None = None,
|
||||
orchestration_id: str | None = None,
|
||||
) -> None:
|
||||
self.message = message
|
||||
self.role = self.coerce_role(role)
|
||||
self.response_format = response_format
|
||||
self.request_response_format = request_response_format
|
||||
self.enable_tool_calls = enable_tool_calls
|
||||
self.thread_id = thread_id
|
||||
self.correlation_id = correlation_id
|
||||
self.created_at = created_at
|
||||
self.orchestration_id = orchestration_id
|
||||
|
||||
@staticmethod
|
||||
def coerce_role(value: Role | str | None) -> Role:
|
||||
"""Normalize various role representations into a Role instance."""
|
||||
if isinstance(value, Role):
|
||||
return value
|
||||
if isinstance(value, str):
|
||||
normalized = value.strip()
|
||||
if not normalized:
|
||||
return Role.USER
|
||||
return Role(value=normalized.lower())
|
||||
return Role.USER
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
"""Convert to dictionary for JSON serialization."""
|
||||
result = {
|
||||
"message": self.message,
|
||||
"enable_tool_calls": self.enable_tool_calls,
|
||||
"role": self.role.value,
|
||||
"request_response_format": self.request_response_format,
|
||||
}
|
||||
if self.response_format:
|
||||
result["response_format"] = serialize_response_format(self.response_format)
|
||||
if self.thread_id:
|
||||
result["thread_id"] = self.thread_id
|
||||
if self.correlation_id:
|
||||
result["correlationId"] = self.correlation_id
|
||||
if self.created_at:
|
||||
result["created_at"] = self.created_at.isoformat()
|
||||
if self.orchestration_id:
|
||||
result["orchestrationId"] = self.orchestration_id
|
||||
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> RunRequest:
|
||||
"""Create RunRequest from dictionary."""
|
||||
created_at = data.get("created_at")
|
||||
if isinstance(created_at, str):
|
||||
try:
|
||||
created_at = datetime.fromisoformat(created_at)
|
||||
except ValueError:
|
||||
created_at = None
|
||||
|
||||
return cls(
|
||||
message=data.get("message", ""),
|
||||
request_response_format=data.get("request_response_format", REQUEST_RESPONSE_FORMAT_TEXT),
|
||||
role=cls.coerce_role(data.get("role")),
|
||||
response_format=_deserialize_response_format(data.get("response_format")),
|
||||
enable_tool_calls=data.get("enable_tool_calls", True),
|
||||
thread_id=data.get("thread_id"),
|
||||
correlation_id=data.get("correlationId"),
|
||||
created_at=created_at,
|
||||
orchestration_id=data.get("orchestrationId"),
|
||||
)
|
||||
Reference in New Issue
Block a user