Skip to content

Commit e1a291e

Browse files
committed
Merge remote-tracking branch 'origin' into aamirj/deferredFunctions
2 parents 623a439 + a49bed2 commit e1a291e

File tree

79 files changed

+5214
-339
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

79 files changed

+5214
-339
lines changed

.github/workflows/release.yml

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,14 @@ on:
66
push:
77
tags:
88
- "*"
9+
workflow_dispatch:
910

1011
# Declare default permissions as read only.
1112
permissions: read-all
1213

14+
env:
15+
EXCLUDE_PACKAGE_FOLDERS: "a2aprotocol"
16+
1317
jobs:
1418
build:
1519
name: Build and Publish
@@ -34,15 +38,8 @@ jobs:
3438
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
3539
with:
3640
python-version: "3.12"
37-
3841
- name: Install uv
39-
run: |
40-
curl -LsSf https://astral.sh/uv/0.5.24/install.sh -o install.sh
41-
echo "f476e445f4a56234fcc12ed478289f80e8e97b230622d8ce2f2406ebfeeb2620 install.sh" > checksum.txt
42-
sha256sum --check checksum.txt
43-
chmod +x install.sh
44-
./install.sh
45-
uv --version
42+
uses: astral-sh/setup-uv@v6
4643

4744
- name: Install dependencies
4845
run: |
@@ -55,4 +52,9 @@ jobs:
5552
5653
- name: Publish to PyPI
5754
run: |
58-
uv publish --trusted-publishing always
55+
shopt -s extglob # Enable extended globbing
56+
EXCLUDE_PATTERN="@($(echo $EXCLUDE_PACKAGE_FOLDERS | sed 's/ /|/g' | sed 's/\([^ ]*\)/microsoft_teams_\1*/g'))"
57+
echo "Excluding pattern: ${EXCLUDE_PATTERN}"
58+
echo "Publishing files:"
59+
ls -1 dist/!(${EXCLUDE_PATTERN})
60+
uv publish --trusted-publishing always dist/!(${EXCLUDE_PATTERN})

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,9 @@ tests/**/.vscode/
3737
tests/**/appPackage/
3838
tests/**/infra/
3939
tests/**/teamsapp*
40+
tests/**/aad.manifest.json
41+
42+
# Node (from tab)
43+
node_modules
44+
dist/
45+
build/

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
> [!CAUTION]
2-
> This project is in active development and not ready for production use. It has not been publicly announced yet.
2+
> This project is in public preview. We’ll do our best to maintain compatibility, but there may be breaking changes in upcoming releases.
33
44
# Microsoft Teams AI Library for Python
55

packages/a2aprotocol/README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Microsoft Teams A2A
2+
3+
<p>
4+
<a href="https://pypi.org/project/microsoft-teams-a2aprotocol/" target="_blank">
5+
<img src="https://img.shields.io/pypi/v/microsoft-teams-a2aprotocol" />
6+
</a>
7+
<a href="https://pypi.org/project/microsoft-teams-a2aprotocol" target="_blank">
8+
<img src="https://img.shields.io/pypi/dw/microsoft-teams-a2aprotocol" />
9+
</a>
10+
</p>
11+
<a href="https://microsoft.github.io/teams-ai" target="_blank">
12+
<img src="https://img.shields.io/badge/📖 Getting Started-blue?style=for-the-badge" />
13+
</a>
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
[project]
2+
name = "microsoft-teams-a2aprotocol"
3+
version = "2.0.0a3"
4+
description = "plugin that enables your teams agent to be used as an a2a agent"
5+
authors = [{ name = "Microsoft", email = "[email protected]" }]
6+
readme = "README.md"
7+
requires-python = ">=3.12"
8+
repository = "https://github.com/microsoft/teams.py"
9+
keywords = ["microsoft", "teams", "ai", "bot", "agents"]
10+
license = "MIT"
11+
classifiers = ["Private :: Do Not Upload"]
12+
dependencies = [
13+
"a2a-sdk[all]>=0.3.7",
14+
"microsoft-teams-common",
15+
"pytest>=8.4.1",
16+
]
17+
18+
[tool.microsoft-teams.metadata]
19+
external = true
20+
21+
22+
[build-system]
23+
requires = ["hatchling"]
24+
build-backend = "hatchling.build"
25+
26+
[tool.hatch.build.targets.wheel]
27+
packages = ["src/microsoft"]
28+
29+
[tool.hatch.build.targets.sdist]
30+
include = ["src"]
31+
32+
[tool.uv.sources]
33+
microsoft-teams-common = { workspace = true }

tests/message-extensions/__init__.py renamed to packages/a2aprotocol/src/microsoft/teams/a2a/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,6 @@
33
Licensed under the MIT License.
44
"""
55

6-
"""Microsoft Teams Message Extensions Test Package."""
6+
7+
def hello() -> str:
8+
return "Hello from a2aprotocol!"
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
"""
2+
Copyright (c) Microsoft Corporation. All rights reserved.
3+
Licensed under the MIT License.
4+
"""
5+
6+
from dataclasses import field
7+
from typing import Any, Callable, List, Optional, Union
8+
9+
from a2a.client import A2AClient
10+
from a2a.types import AgentCard, Message, Task
11+
from microsoft.teams.common import ConsoleLogger
12+
from pydantic import BaseModel
13+
14+
15+
class FunctionMetadata(BaseModel):
16+
name: str
17+
description: str
18+
19+
20+
class AgentPromptParams(BaseModel):
21+
card: AgentCard
22+
client: A2AClient
23+
24+
25+
class BuildPromptMetadata(BaseModel):
26+
system_prompt: Optional[str] = None
27+
agent_details: List[AgentPromptParams] = field(default_factory=lambda: [])
28+
29+
30+
class BuildMessageForAgentMetadata(BaseModel):
31+
card: AgentCard
32+
input: str
33+
metadata: Optional[dict[str, Any]] = None
34+
35+
36+
class BuildMessageFromAgentMetadata(BaseModel):
37+
card: AgentCard
38+
response: Union[Task, Message]
39+
original_input: str
40+
41+
42+
BuildFunctionMetadata = Callable[[AgentCard], FunctionMetadata]
43+
BuildPrompt = Callable[[BuildPromptMetadata], Optional[str]]
44+
BuildMessageForAgent = Callable[[BuildMessageForAgentMetadata], Union[Message, str]]
45+
BuildMessageFromAgentResponse = Callable[[BuildMessageFromAgentMetadata], str]
46+
47+
48+
class A2AClientPluginOptions(BaseModel):
49+
"""
50+
Options for constructing an A2AClientPlugin using the official SDK.
51+
"""
52+
53+
build_function_metadata: Optional[BuildFunctionMetadata] = None
54+
"Optional function to customize the function name and description for each agent card."
55+
build_prompt: Optional[BuildPrompt] = None
56+
"Optional function to customize the prompt given all agent cards."
57+
build_message_for_agent: Optional[BuildMessageForAgent] = None
58+
"Optional function to customize the message format sent to each agent."
59+
build_message_from_agent_response: Optional[BuildMessageFromAgentResponse] = None
60+
"Optional function to customize how agent responses are processed into strings."
61+
logger: Optional[ConsoleLogger] = None
62+
"The associated logger"
63+
64+
65+
class A2APluginUseParams(BaseModel):
66+
"""
67+
Parameters for registering an agent with the A2AClientPlugin.
68+
"""
69+
70+
key: str
71+
"Unique key to identify this agent"
72+
card_url: str
73+
"URL to the agent's card endpoint"
74+
build_function_metadata: Optional[BuildFunctionMetadata] = None
75+
"Custom function metadata builder for this specific agent"
76+
build_message_for_agent: Optional[BuildMessageForAgent] = None
77+
"Custom message builder for this specific agent"
78+
build_message_from_agent_response: Optional[BuildMessageFromAgentResponse] = None
79+
"Custom response processor for this specific agent"

packages/ai/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "microsoft-teams-ai"
3-
version = "2.0.0a1"
3+
version = "2.0.0a3"
44
description = "package to handle interacting with ai or llms"
55
authors = [{ name = "Microsoft", email = "[email protected]" }]
66
readme = "README.md"

packages/ai/src/microsoft/teams/ai/__init__.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,14 @@
66
from .agent import Agent
77
from .ai_model import AIModel
88
from .chat_prompt import ChatPrompt, ChatSendResult
9-
from .function import DeferredResult, Function, FunctionCall
9+
from .function import (
10+
DeferredResult,
11+
Function,
12+
FunctionCall,
13+
FunctionHandler,
14+
FunctionHandlers,
15+
FunctionHandlerWithNoParams,
16+
)
1017
from .memory import ListMemory, Memory
1118
from .message import DeferredMessage, FunctionMessage, Message, ModelMessage, SystemMessage, UserMessage
1219

@@ -26,4 +33,7 @@
2633
"Memory",
2734
"ListMemory",
2835
"AIModel",
36+
"FunctionHandler",
37+
"FunctionHandlerWithNoParams",
38+
"FunctionHandlers",
2939
]

packages/ai/src/microsoft/teams/ai/chat_prompt.py

Lines changed: 70 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@
77
from dataclasses import dataclass
88
from inspect import isawaitable
99
from logging import Logger
10-
from typing import Any, Awaitable, Callable, Self, TypeVar, cast
10+
from typing import Any, Awaitable, Callable, Dict, Optional, Self, TypeVar, Union, cast, overload
1111

1212
from microsoft.teams.common.logging import ConsoleLogger
1313
from pydantic import BaseModel
1414

1515
from .ai_model import AIModel
16-
from .function import Function, FunctionHandler
16+
from .function import Function, FunctionHandler, FunctionHandlers, FunctionHandlerWithNoParams
1717
from .memory import Memory
1818
from .message import DeferredMessage, FunctionMessage, Message, ModelMessage, SystemMessage, UserMessage
1919
from .plugin import AIPluginProtocol
@@ -70,17 +70,67 @@ def __init__(
7070
self.logger = logger or ConsoleLogger().create_logger("@teams/ai/chat_prompt")
7171
self.instructions = instructions
7272

73-
def with_function(self, function: Function[T]) -> Self:
73+
@overload
74+
def with_function(self, function: Function[T]) -> Self: ...
75+
76+
@overload
77+
def with_function(
78+
self,
79+
*,
80+
name: str,
81+
description: str,
82+
parameter_schema: Union[type[T], Dict[str, Any]],
83+
handler: FunctionHandlers,
84+
) -> Self: ...
85+
86+
@overload
87+
def with_function(
88+
self,
89+
*,
90+
name: str,
91+
description: str,
92+
handler: FunctionHandlerWithNoParams,
93+
) -> Self: ...
94+
95+
def with_function(
96+
self,
97+
function: Function[T] | None = None,
98+
*,
99+
name: str | None = None,
100+
description: str | None = None,
101+
parameter_schema: Union[type[T], Dict[str, Any], None] = None,
102+
handler: FunctionHandlers | None = None,
103+
) -> Self:
74104
"""
75105
Add a function to the available functions for this prompt.
76106
107+
Can be called in three ways:
108+
1. with_function(function=Function(...))
109+
2. with_function(name=..., description=..., parameter_schema=..., handler=...)
110+
3. with_function(name=..., description=..., handler=...) - for functions with no parameters
111+
77112
Args:
78-
function: Function to add to the available functions
113+
function: Function object to add (first overload)
114+
name: Function name (second and third overload)
115+
description: Function description (second and third overload)
116+
parameter_schema: Function parameter schema (second overload, optional)
117+
handler: Function handler (second and third overload)
79118
80119
Returns:
81120
Self for method chaining
82121
"""
83-
self.functions[function.name] = function
122+
if function is not None:
123+
self.functions[function.name] = function
124+
else:
125+
if name is None or description is None or handler is None:
126+
raise ValueError("When not providing a Function object, name, description, and handler are required")
127+
func = Function[T](
128+
name=name,
129+
description=description,
130+
parameter_schema=parameter_schema,
131+
handler=handler,
132+
)
133+
self.functions[func.name] = func
84134
return self
85135

86136
def with_plugin(self, plugin: AIPluginProtocol) -> Self:
@@ -259,9 +309,7 @@ async def on_chunk_fn(chunk: str):
259309

260310
return ChatSendResult(response=current_response)
261311

262-
def _wrap_function_handler(
263-
self, original_handler: FunctionHandler[BaseModel], function_name: str
264-
) -> FunctionHandler[BaseModel]:
312+
def _wrap_function_handler(self, original_handler: FunctionHandlers, function_name: str) -> FunctionHandlers:
265313
"""
266314
Wrap a function handler with plugin before/after hooks.
267315
@@ -276,20 +324,28 @@ def _wrap_function_handler(
276324
Wrapped handler that includes plugin hook execution
277325
"""
278326

279-
async def wrapped_handler(params: BaseModel) -> str:
327+
async def wrapped_handler(params: Optional[BaseModel]) -> str:
280328
# Run before function call hooks
281329
for plugin in self.plugins:
282330
await plugin.on_before_function_call(function_name, params)
283331

284-
# Call the original function (could be sync or async)
285-
result = original_handler(params)
286-
if isawaitable(result):
287-
result = await result
332+
if params:
333+
# Call the original function with params (could be sync or async)
334+
casted_handler = cast(FunctionHandler[BaseModel], original_handler)
335+
result = casted_handler(params)
336+
if isawaitable(result):
337+
result = await result
338+
else:
339+
# Function with no parameters case
340+
casted_handler = cast(FunctionHandlerWithNoParams, original_handler)
341+
result = casted_handler()
342+
if isawaitable(result):
343+
result = await result
288344

289345
# Run after function call hooks
290346
current_result = result
291347
for plugin in self.plugins:
292-
plugin_result = await plugin.on_after_function_call(function_name, params, current_result)
348+
plugin_result = await plugin.on_after_function_call(function_name, current_result, params)
293349
if plugin_result is not None:
294350
current_result = plugin_result
295351

0 commit comments

Comments
 (0)