Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions packages/ai/src/microsoft/teams/ai/chat_prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import inspect
from dataclasses import dataclass
from inspect import isawaitable
from typing import Any, Awaitable, Callable, Optional, Self, TypeVar, cast
from typing import Any, Awaitable, Callable, Optional, Self, cast

from pydantic import BaseModel

Expand All @@ -16,8 +16,6 @@
from .message import Message, ModelMessage, SystemMessage, UserMessage
from .plugin import AIPluginProtocol

T = TypeVar("T", bound=BaseModel)


@dataclass
class ChatSendResult:
Expand Down Expand Up @@ -58,7 +56,7 @@ def __init__(
self.functions: dict[str, Function[Any]] = {func.name: func for func in functions} if functions else {}
self.plugins: list[AIPluginProtocol] = plugins or []

def with_function(self, function: Function[T]) -> Self:
def with_function(self, function: Function[BaseModel]) -> Self:
"""
Add a function to the available functions for this prompt.

Expand Down
27 changes: 25 additions & 2 deletions packages/openai/src/microsoft/teams/openai/function_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from typing import Any, Dict, Optional

from microsoft.teams.ai import Function
from pydantic import BaseModel, create_model
from pydantic import BaseModel, Field


def get_function_schema(func: Function[BaseModel]) -> Dict[str, Any]:
Expand Down Expand Up @@ -53,7 +53,30 @@ def parse_function_arguments(func: Function[BaseModel], arguments: Dict[str, Any

if isinstance(func.parameter_schema, dict):
# For dict schemas, create a simple BaseModel dynamically
DynamicModel = create_model("DynamicParams")
schema = func.parameter_schema
props = schema.get("properties", {})
required = set(schema.get("required", []))

def map_type(json_type: str) -> Any:
"""Map JSON Schema types to Python types"""
return {
"string": str,
"integer": int,
"number": float,
"boolean": bool,
"array": list,
"object": dict,
}.get(json_type, Any)

attrs: Dict[str, Any] = {}
annotations: Dict[str, Any] = {}
for name, details in props.items():
annotations[name] = map_type(details.get("type"))
default = ... if name in required else None
attrs[name] = Field(default=default, description=details.get("description", ""))

DynamicModel = type("DynamicParams", (BaseModel,), {"__annotations__": annotations, **attrs})
Copy link

Copilot AI Oct 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using type() to dynamically create classes can be hard to debug and understand. Consider using create_model() from Pydantic which was already imported and provides better error handling and validation features.

Copilot uses AI. Check for mistakes.

return DynamicModel(**arguments)
else:
# For Pydantic model schemas, parse normally
Expand Down
21 changes: 12 additions & 9 deletions tests/ai-test/src/handlers/function_calling.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,6 @@
from pydantic import BaseModel


class SearchPokemonParams(BaseModel):
pokemon_name: str
"""The name of the pokemon."""


class GetLocationParams(BaseModel):
"""No parameters needed for location"""

Expand All @@ -30,13 +25,16 @@ class GetWeatherParams(BaseModel):
"""The location to get weather for"""


async def pokemon_search_handler(params: SearchPokemonParams) -> str:
async def pokemon_search_handler(params: BaseModel) -> str:
"""Search for Pokemon using PokeAPI - matches documentation example"""
try:
pokemon_name = getattr(params, "pokemon_name", None)
if not pokemon_name:
raise ValueError("Missing required parameter 'pokemon_name'")
Comment on lines +31 to +33
Copy link

Copilot AI Oct 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using getattr() with manual validation duplicates the validation that Pydantic models provide automatically. This approach is more error-prone and less maintainable than using a proper schema validation.

Copilot uses AI. Check for mistakes.
async with aiohttp.ClientSession() as session:
async with session.get(f"https://pokeapi.co/api/v2/pokemon/{params.pokemon_name.lower()}") as response:
async with session.get(f"https://pokeapi.co/api/v2/pokemon/{pokemon_name.lower()}") as response:
if response.status != 200:
raise ValueError(f"Pokemon '{params.pokemon_name}' not found")
raise ValueError(f"Pokemon '{pokemon_name}' not found")

data = await response.json()

Expand All @@ -55,11 +53,16 @@ async def pokemon_search_handler(params: SearchPokemonParams) -> str:
async def handle_pokemon_search(model: AIModel, ctx: ActivityContext[MessageActivity]) -> None:
"""Handle single function calling - Pokemon search"""
agent = Agent(model)

agent.with_function(
Function(
name="pokemon_search",
description="Search for pokemon information including height, weight, and types",
parameter_schema=SearchPokemonParams,
parameter_schema={
"type": "object",
"properties": {"pokemon_name": {"type": "string"}},
"required": ["pokemon_name"],
},
handler=pokemon_search_handler,
)
)
Expand Down