Sitemap
Krishnan Sriram

Krishnan Sriram

Tech enthusiast, life long learner and an enterprise architect

Cloud Foundry — Agentic AI in Azure 2

13 min readApr 2, 2026

--

Press enter or click to view image in full size
Photo by benjamin lehman on Unsplash

Let’s continue from our previous article and build more agents/tools. In this section we’ll see how to build multiple function tool agents, try more different types of agents and see how they work.

Multiple FunctionTools — If I can do 2 functions tools, I can do more

We’ll extend the great weather functionality we built in our previous article with future predictions as well. Let’s create a new file weather_agent02.py and add these contents

I know you sigh….yet another weather forecast

“I created three weather agents for redundancy — because when one is wrong, I like having two others confidently disagree with it.”

# weather_agent02.py
# Dependencies: azure-ai-projects==1.0.0, azure-ai-agents==1.1.0
import os
import json
import requests
from dotenv import load_dotenv
from azure.identity import DefaultAzureCredential
from azure.ai.projects import AIProjectClient
from azure.ai.agents.models import (
FunctionTool,
ToolSet,
Agent,
AgentThread,
ThreadRun,
MessageRole,
)

load_dotenv()


def get_current_weather(city: str, unit: str = "imperial") -> str:
"""
Fetches current weather for a given city.
Falls back to mock data if no API key is set.
"""
api_key = os.getenv("OPENWEATHER_API_KEY")

if api_key:
# Real OpenWeatherMap API call
url = (
f"https://api.openweathermap.org/data/2.5/weather"
f"?q={city}&units={unit}&appid={api_key}"
)
try:
response = requests.get(url, timeout=5)
response.raise_for_status()
data = response.json()
return json.dumps({
"city": data["name"],
"temperature": f"{data['main']['temp']}°{'F' if unit == 'imperial' else 'C'}",
"feels_like": f"{data['main']['feels_like']}°{'F' if unit == 'imperial' else 'C'}",
"condition": data["weather"][0]["description"],
"humidity": f"{data['main']['humidity']}%",
"wind_speed": f"{data['wind']['speed']} mph",
})
except requests.RequestException as e:
return json.dumps({"error": f"API call failed: {str(e)}"})
else:
# 🌧️ Mock data — because it's always raining in London
mock_data = {
"london": {"temperature": "55°F", "condition": "light rain", "humidity": "82%", "wind_speed": "12 mph"},
"chicago": {"temperature": "28°F", "condition": "heavy snow", "humidity": "91%", "wind_speed": "25 mph"},
"dublin": {"temperature": "48°F", "condition": "cloudy", "humidity": "78%", "wind_speed": "15 mph"},
"sydney": {"temperature": "77°F", "condition": "sunny", "humidity": "55%", "wind_speed": "8 mph"},
}
city_lower = city.lower()
weather = mock_data.get(city_lower, {
"temperature": "72°F",
"condition": "clear skies",
"humidity": "60%",
"wind_speed": "5 mph",
})
return json.dumps({"city": city, **weather, "source": "mock"})


def get_weather_forecast(city: str, days: int = 3) -> str:
"""
Returns a simple multi-day forecast for a city.
Days must be between 1 and 5.
"""
days = max(1, min(days, 5)) # clamp to 1–5

api_key = os.getenv("OPENWEATHER_API_KEY")

if api_key:
url = (
f"https://api.openweathermap.org/data/2.5/forecast"
f"?q={city}&cnt={days * 8}&appid={api_key}&units=imperial"
)
try:
response = requests.get(url, timeout=5)
response.raise_for_status()
data = response.json()
# Grab one reading per day (every 8th entry = ~24hrs apart)
forecast = []
for i in range(0, min(days * 8, len(data["list"])), 8):
entry = data["list"][i]
forecast.append({
"date": entry["dt_txt"].split(" ")[0],
"temperature": f"{entry['main']['temp']}°F",
"condition": entry["weather"][0]["description"],
})
return json.dumps({"city": city, "forecast": forecast})
except requests.RequestException as e:
return json.dumps({"error": f"API call failed: {str(e)}"})
else:
# Mock forecast
mock_forecast = [
{"date": "2026-03-27", "temperature": "57°F", "condition": "partly cloudy"},
{"date": "2026-03-28", "temperature": "61°F", "condition": "sunny"},
{"date": "2026-03-29", "temperature": "53°F", "condition": "light rain"},
{"date": "2026-03-30", "temperature": "49°F", "condition": "overcast"},
{"date": "2026-03-31", "temperature": "55°F", "condition": "sunny"},
]
return json.dumps({
"city": city,
"forecast": mock_forecast[:days],
"source": "mock",
})


weather_tool = FunctionTool(
functions={get_current_weather, get_weather_forecast}
)


def create_project_client() -> AIProjectClient:
client = AIProjectClient(
endpoint=os.environ["PROJECT_ENDPOINT"],
credential=DefaultAzureCredential(),
)
print("✅ Connected to Foundry Project")
return client


def create_weather_agent(client: AIProjectClient) -> Agent:
toolset = ToolSet()
toolset.add(weather_tool)

agent = client.agents.create_agent(
model=os.environ["MODEL_DEPLOYMENT_NAME"],
name="weather-reporter",
instructions="""
You are a friendly weather reporting assistant.
You have two tools:
- get_current_weather: use when the user asks about current conditions
- get_weather_forecast: use when the user asks about upcoming days
Always mention the city name in your response.
If the data source is 'mock', let the user know it's simulated data.
Keep responses concise and conversational.
""",
toolset=toolset,
)
print(f"✅ Weather agent created: {agent.id}")
return agent


def ask_agent(
client: AIProjectClient,
thread: AgentThread,
agent: Agent,
question: str,
) -> None:
print(f"\n👤 You: {question}")

client.agents.messages.create(
thread_id=thread.id,
role=MessageRole.USER,
content=question,
)

run = client.agents.runs.create_and_process(
thread_id=thread.id,
agent_id=agent.id,
)

if run.status == "completed":
messages = client.agents.messages.list(thread_id=thread.id)
for msg in messages:
if msg.role == "assistant":
for block in msg.content:
if hasattr(block, "text"):
print(f"🤖 Agent: {block.text.value}")
break
else:
print(f"❌ Run failed: {run.status}")
if run.last_error:
print(f" Error: {run.last_error.message}")


def main():
client = create_project_client()
agent = create_weather_agent(client)
thread = client.agents.threads.create()
print(f"✅ Thread created: {thread.id}")

# Ask a few questions in the same thread — agent remembers context
ask_agent(client, thread, agent, "What's the weather like in London right now?")
ask_agent(client, thread, agent, "Give me a 3-day forecast for Sydney.")
ask_agent(client, thread, agent, "Should I pack an umbrella for Chicago tomorrow?")

# Cleanup
client.agents.delete_agent(agent.id)
print("\n🧹 Agent cleaned up")


if __name__ == "__main__":
main()

Most of this code is same as previous except for 2 things. We have added a new method get_weather_forecast and ask_agent method uses create_and_process as opposed to create method. get_weather_forecast method is self explanatory. One thing you may want to know when you build agents in CF (Cloud foundry) is

create()              =  start the run, return immediately, YOU manage what happens next
create_and_process() = start the run, wait until fully done, return the final result
Press enter or click to view image in full size

This final chart can help you decide where to use what

create_and_process()  can only handle tools that run on Azure's servers
create() is required for tools that run on YOUR machine
FunctionTool runs on YOUR machine → use create()
SDK has no way to reach into your
Python process and run your function

CodeInterpreterTool runs on AZURE → use create_and_process()
FileSearchTool runs on AZURE → use create_and_process()
BingGroundingTool runs on AZURE → use create_and_process()
AzureAISearchTool runs on AZURE → use create_and_process()

Back to the code we put together…..Here’s what we are doing in the code

The script builds a conversational weather assistant on top of Azure AI Foundry. It defines two local Python functions — get_current_weather and get_weather_forecast — that either call the real OpenWeatherMap API or fall back to hardcoded mock data if no API key is set. Those functions get wrapped in a FunctionTool and registered with a cloud-hosted agent that knows when to invoke them based on what the user asks.

The key design challenge here is that FunctionTool requires local dispatch — when the agent decides to call a function, Azure sends back a requires_action signal and your code has to execute the Python function locally and submit the result back. That's why ask_agent runs a manual polling loop instead of the simpler create_and_process. The thread persists across all three questions, so the agent has full conversation context throughout.

Press enter or click to view image in full size

The most important thing the diagram shows is the requires_action loop in the middle — that back-and-forth between the local machine and Azure is what makes FunctionTool different from server-side tools like Bing. Azure tells your code "I need the result of this function", your Python runs it locally, and you submit the result back before the run can continue. Every node is clickable if you want to explore any part further.

Here’s what you see upon execution uv run weather_agent02.py


✅ Connected to Foundry Project
✅ Weather agent created: asst_Wi6dLBSk9rDx3PmCKX10dXOx
✅ Thread created: thread_3jlXDp25qkznAmR8lRD0WAh5

👤 You: What's the weather like in London right now?
🤖 Agent: Right now in London, the weather is 55°F with light rain. Humidity is at 82%, and there's a wind speed of 12 mph. Please note, this is simulated data.

👤 You: Give me a 3-day forecast for Sydney.
🤖 Agent: Here’s a 3-day forecast for Sydney (simulated data):

- March 27: Partly cloudy, 57°F
- March 28: Sunny, 61°F
- March 29: Light rain, 53°F

Let me know if you need more details!

👤 You: Should I pack an umbrella for Chicago tomorrow?
🤖 Agent: You won't need an umbrella for Chicago tomorrow—it’s expected to be partly cloudy. Please note, this is simulated data. Safe travels!

🧹 Agent cleaned up

Bing Search

We are talking how to use a bing search as a tool without UI. On lighter note we all had experience with bing and can relate to this, we all have seen this, haven’t we

The Browser Logic

Chrome: “I noticed you typed ‘ho,’ so I’ve already pre-loaded the documentation for the React Hooks you were looking for. Please, take a seat.”

Bing: “I noticed you typed ‘Google.’ Are you lost? Is everything okay at home? Look, I have points! If you stay here and click on this picture of a baby seal, I’ll give you enough credits to buy a $5 Amazon gift card in the year 2029. Please don’t leave me for the ‘Google’ again.”

Press enter or click to view image in full size
Photo by appshunter.io on Unsplash

In order for Bing search to work, you need to create connection over to Bing search service. And the way you do it is listed as high level steps

Azure Portal → Bing Search → Create
Name: bing-search-resource
Pricing: S1 (supports grounding)
→ Create

Foundry Portal → Your Project → Settings → Connections → + New Connection
Type: Grounding with Bing Search
Name: bing-search-connection ← this name goes in your .env
Resource: bing-search-resource
→ Save

If you’d like pictures as a way to guide you, here you go

Get Krishnan Sriram’s stories in your inbox

Join Medium for free to get updates from this writer.

from your Project, go all the way to Management Center, then choose Connected resources, tap on New Connection, in the search bar, type bing and choose grounding with bing search, if you don’t have one created already, tap on create a new resource, follow the prompts and finally tap on Add Connection

Press enter or click to view image in full size
Press enter or click to view image in full size
Press enter or click to view image in full size
Press enter or click to view image in full size
Press enter or click to view image in full size
Press enter or click to view image in full size
Press enter or click to view image in full size
Press enter or click to view image in full size

Create a new file bing_search.py

Here’s what we are doing in the code below

  1. Environment configuration — load_dotenv() reads your .env file at startup, making PROJECT_ENDPOINT, MODEL_DEPLOYMENT_NAME, and BING_CONNECTION_NAME available via os.environ without hardcoding secrets.
  2. Passwordless auth via DefaultAzureCredential — Rather than using API keys, the client authenticates using Azure's identity chain (local Azure CLI login in dev, managed identity in production), so no credentials ever touch the code.
  3. AIProjectClient as the central hub — This single client object is the entry point to everything: connections, agents, threads, messages, and runs. All other operations flow through it.
  4. Two-step Bing tool setup — First client.connections.get(name=...) resolves your named Bing connection from the Foundry project into a connection ID, then BingGroundingTool(connection_id=...) wraps that ID into a tool definition the agent can use. The indirection exists because the same resource can have multiple named connections.
  5. Agent is stateless and reusable — create_agent registers a persistent agent on Azure with a fixed model, name, instructions, and tools. It lives in the cloud and can be reused across many threads and sessions until explicitly deleted.
  6. Threads provide conversation memory — A thread is a managed conversation container. All messages from a session are stored in it, so the agent has full context across multiple ask_agent calls without you manually managing history.
  7. bing_tool.definitions wires the tool server-side — Passing tools=bing_tool.definitions tells Azure which tools the agent is allowed to invoke. Because Bing grounding executes entirely on Azure's infrastructure, there's no local function dispatch needed (unlike the FunctionTool case from earlier).
  8. create_and_process is safe here — This convenience method starts a run, polls until completion, and handles the requires_action loop internally. It works correctly for server-side tools like Bing because Azure handles tool execution itself — no local Python callbacks are needed.
  9. Message parsing walks content blocks — The response is a list of content blocks (text, citations, images, etc.). The code filters for blocks with a .text attribute, which is where the assistant's prose response lives, and prints block.text.value.
  10. Explicit cleanup with delete_agent — Agents persist in your Foundry project until deleted and count against quotas. Calling delete_agent at the end is good hygiene, especially during development where you're creating many throwaway agents.

import os
from dotenv import load_dotenv
from azure.identity import DefaultAzureCredential
from azure.ai.projects import AIProjectClient
from azure.ai.agents.models import (
BingGroundingTool,
MessageRole,
Agent,
AgentThread,
)

load_dotenv()


def create_project_client() -> AIProjectClient:
client = AIProjectClient(
endpoint=os.environ["PROJECT_ENDPOINT"],
credential=DefaultAzureCredential(),
)
print("✅ Connected to Foundry Project")
return client


def create_bing_tool(client: AIProjectClient) -> BingGroundingTool:
# Step 1: look up the connection by name from your project
bing_connection = client.connections.get(
name=os.environ["BING_CONNECTION_NAME"]
)
print(f"✅ Bing connection found: {bing_connection.id}")

# Step 2: wrap it as a tool using the connection ID
return BingGroundingTool(connection_id=bing_connection.id)


def create_bing_agent(
client: AIProjectClient,
bing_tool: BingGroundingTool,
) -> Agent:
agent = client.agents.create_agent(
model=os.environ["MODEL_DEPLOYMENT_NAME"],
name="news-reporter",
instructions="""
You are a helpful news and research assistant with access to
real-time web search via Bing.
Always search for current information before answering.
Include source citations in your response where available.
Keep responses concise and well structured.
""",
tools=bing_tool.definitions, # ← same pattern as CodeInterpreterTool
)
print(f"✅ Agent created: {agent.id}")
return agent


def ask_agent(
client: AIProjectClient,
thread: AgentThread,
agent: Agent,
question: str,
) -> None:
print(f"\n👤 You: {question}")

client.agents.messages.create(
thread_id=thread.id,
role=MessageRole.USER,
content=question,
)

# ✅ create_and_process works here because Bing runs server-side
run = client.agents.runs.create_and_process(
thread_id=thread.id,
agent_id=agent.id,
)

if run.status == "completed":
messages = client.agents.messages.list(thread_id=thread.id)
for msg in messages:
if msg.role == "assistant":
for block in msg.content:
if hasattr(block, "text"):
print(f"\n🤖 Agent: {block.text.value}")
break
else:
print(f"❌ Run failed: {run.status}")
if run.last_error:
print(f" Error: {run.last_error.message}")


def main():
client = create_project_client()
bing_tool = create_bing_tool(client)
agent = create_bing_agent(client, bing_tool)
thread = client.agents.threads.create()
print(f"✅ Thread created: {thread.id}")

ask_agent(client, thread, agent, "What are the top tech news stories today?")
ask_agent(client, thread, agent, "What is the latest version of Python?")
ask_agent(client, thread, agent, "Summarise the most recent Azure AI announcements.")

client.agents.delete_agent(agent.id)
print("\n🧹 Agent cleaned up")


if __name__ == "__main__":
main()

Run it with the command uv run bing_search.py. You should see the following results


✅ Connected to Foundry Project
✅ Bing connection found: /subscriptions/a509ec7e-2297-419d-8587-ca08543e3cbe/resourceGroups/rg-eus2-agenticai-dev-01/providers/Microsoft.CognitiveServices/accounts/cf-eus2-agenticai-dev-01/projects/cfp-eus2-agenticai-dev-01/connections/bseus2agenticaidev01
✅ Agent created: asst_NElI6KGB3sDElcbIpdclyttK
✅ Thread created: thread_1nAYYcd3D2xi5TSnp3naER4r

👤 You: What are the top tech news stories today?

🤖 Agent: Here are some of the top tech news stories today:

1. **Apple Celebrates 50 Years**: Apple is marking its 50th anniversary, with retrospectives on its journey from a garage startup to a global tech titan. Engineers reflect on Apple's biggest product successes and innovations over the decades【3:0†source】.

2. **Gmail Username Changes**: Google now allows US users to change their Gmail address, a long-requested feature. Guides and updates are rolling out on how to make this change【3:0†source】.

3. **iOS Security Alerts**: Apple is pushing rare security patches to older devices, warning about new attacks targeting outdated iOS software. Users are being advised to update devices to protect against exploits【3:0†source】.

4. **Anthropic Source Code Leak**: AI company Anthropic experienced a leak of part of its Claude AI internal source code on GitHub, prompting rapid takedowns. The incident highlights ongoing security concerns in tech【3:1†source】【3:2†source】.

5. **Crypto Asset Manager CoinShares to Trade on Nasdaq**: CoinShares will begin trading via a SPAC merger, showcasing ongoing activity in crypto and fintech mergers【3:1†source】.

6. **SpaceX IPO Speculation**: Reports indicate SpaceX may be preparing for a confidential IPO, potentially valued at $1.75 trillion, which could significantly impact the tech and space industries【3:2†source】.

7. **AWS Outage Impact**: Recently, Amazon Web Services suffered a significant outage affecting millions of users worldwide, raising awareness about the fragility of cloud computing infrastructure【3:3†source】.

If you need more details on any story, let me know!

👤 You: What is the latest version of Python?

🤖 Agent: The latest stable version of Python as of April 2026 is **Python 3.14.3**, released on February 3, 2026. This update includes nearly 300 bug fixes and improvements over the previous release. Python 3.14 introduced notable features such as free-threaded support, template string literals, deferred evaluation of annotations, and Zstandard compression support. Upgrading to the latest version ensures optimal performance, security, and compatibility with modern libraries and frameworks【7:2†source】【7:0†source】.

👤 You: Summarise the most recent Azure AI announcements.

🤖 Agent: Here’s a summary of the most recent Azure AI announcements and updates:

1. **Microsoft Fabric FabCon 2026**: The event highlighted Fabric’s evolution from a data platform to an enterprise intelligence platform. Key new features include Fabric IQ (contextual intelligence for AI), graph database integration, AI-native architectures, and real-time intelligence enhancements. Data agents and unified governance (OneLake) were emphasized, signaling a move toward AI-powered, real-time decision-making in enterprise operations【11:3†source】.

2. **New AI Certifications**: Microsoft announced a new set of AI-focused certifications for 2026 (AI‑300, AI‑103, AI‑200, SC‑500, etc.) and the retirement of several older ones (DP‑100, AI‑900, AI‑102, AZ‑204, AZ‑500), reflecting changing demands for enterprise AI and cloud skills【11:2†source】.

3. **Azure SDK & Developer CLI Updates**: Azure Developer CLI (azd) now includes features to run and debug AI agents locally, integration with GitHub Copilot, and container app jobs—streamlining AI development and workflow automation【11:1†source】.

4. **Integration and Security Enhancements**: Azure announced new capabilities in Cosmos DB Mirroring (with private endpoints) and Fabric Mirroring for MySQL, unlocking analytics and real-time dashboards while maintaining strong network security—important for organizations handling sensitive and regulated data【11:0†source】.

5. **Upcoming Service Retirements**: Azure Machine Learning’s external data import and data connections will be retired later this year, aligning with evolving product strategies【11:1†source】.

These updates highlight Microsoft’s strategic transition toward unified, AI-native enterprise platforms, improved developer experience, expanded certifications, and heightened network security for AI data workloads.

🧹 Agent cleaned up

You can find all the code used in this article here

Next steps

  • File Search and AI Search tools
  • How do we build a simple LangChain/LangGraph solution
  • What about Agent to Agent communication and how secure is the communication
  • Understanding messages and threads and more

As always, thanks for taking time. Until next….stay safe

--

--