Skip to content

Commit 09b67ea

Browse files
Merge pull request #520 from microsoft/psl-rc-agentm-dev
feat: Agent and thread management
2 parents d592c69 + dfd4846 commit 09b67ea

File tree

11 files changed

+379
-181
lines changed

11 files changed

+379
-181
lines changed

infra/deploy_app_service.bicep

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ param AzureOpenAIModel string
3131
@description('Azure Open AI Endpoint')
3232
param AzureOpenAIEndpoint string = ''
3333

34+
param azureAiAgentApiVersion string
3435
param azureOpenAIApiVersion string
3536
param azureOpenaiResource string = ''
3637
param USE_CHAT_HISTORY_ENABLED string = ''
@@ -267,7 +268,11 @@ resource Website 'Microsoft.Web/sites@2020-06-01' = {
267268
}
268269
{
269270
name: 'AZURE_AI_AGENT_API_VERSION'
270-
value: azureOpenAIApiVersion
271+
value: azureAiAgentApiVersion
272+
}
273+
{
274+
name: 'SOLUTION_NAME'
275+
value: solutionName
271276
}
272277
{
273278
name: 'USE_CHAT_HISTORY_ENABLED'

infra/main.bicep

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ param gptModelVersion string = '2025-04-14'
4343
@description('API version for Azure OpenAI service. This should be a valid API version supported by the service.')
4444
param azureOpenaiAPIVersion string = '2025-01-01-preview'
4545

46+
47+
@description('API version for Azure AI Agent service. This should be a valid API version supported by the service.')
48+
param azureAiAgentApiVersion string = '2025-05-01'
49+
4650
@minValue(10)
4751
@description('Capacity of the GPT deployment:')
4852
// You can increase this, but capacity is limited per model/region, so you will get errors if you go over
@@ -155,6 +159,7 @@ module appserviceModule 'deploy_app_service.bicep' = {
155159
solutionLocation: solutionLocation
156160
aiSearchService: aifoundry.outputs.aiSearchService
157161
aiSearchName: aifoundry.outputs.aiSearchName
162+
azureAiAgentApiVersion: azureAiAgentApiVersion
158163
AzureOpenAIEndpoint: aifoundry.outputs.aoaiEndpoint
159164
AzureOpenAIModel: gptModelName
160165
azureOpenAIApiVersion: azureOpenaiAPIVersion //'2024-02-15-preview'

infra/main.json

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"_generator": {
66
"name": "bicep",
77
"version": "0.36.177.2456",
8-
"templateHash": "12401520506444829394"
8+
"templateHash": "7421251462011771854"
99
}
1010
},
1111
"parameters": {
@@ -78,6 +78,13 @@
7878
"description": "API version for Azure OpenAI service. This should be a valid API version supported by the service."
7979
}
8080
},
81+
"azureAiAgentApiVersion": {
82+
"type": "string",
83+
"defaultValue": "2025-05-01",
84+
"metadata": {
85+
"description": "API version for Azure AI Agent service. This should be a valid API version supported by the service."
86+
}
87+
},
8188
"gptDeploymentCapacity": {
8289
"type": "int",
8390
"defaultValue": 150,
@@ -354,8 +361,7 @@
354361
},
355362
"abbrs": "[variables('$fxv#0')]",
356363
"solutionLocation": "[if(empty(parameters('AZURE_LOCATION')), resourceGroup().location, parameters('AZURE_LOCATION'))]",
357-
"uniqueId": "[toLower(uniqueString(parameters('environmentName'), subscription().id, variables('solutionLocation')))]",
358-
"solutionPrefix": "[format('dg{0}', padLeft(take(variables('uniqueId'), 12), 12, '0'))]"
364+
"solutionPrefix": "[format('dg{0}', padLeft(take(toLower(uniqueString(subscription().id, parameters('environmentName'), resourceGroup().location, resourceGroup().name)), 12), 12, '0'))]"
359365
},
360366
"resources": [
361367
{
@@ -1780,6 +1786,9 @@
17801786
"aiSearchName": {
17811787
"value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_ai_foundry'), '2022-09-01').outputs.aiSearchName.value]"
17821788
},
1789+
"azureAiAgentApiVersion": {
1790+
"value": "[parameters('azureAiAgentApiVersion')]"
1791+
},
17831792
"AzureOpenAIEndpoint": {
17841793
"value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_ai_foundry'), '2022-09-01').outputs.aoaiEndpoint.value]"
17851794
},
@@ -1839,7 +1848,7 @@
18391848
"_generator": {
18401849
"name": "bicep",
18411850
"version": "0.36.177.2456",
1842-
"templateHash": "4427077770927711325"
1851+
"templateHash": "16850060889438240970"
18431852
}
18441853
},
18451854
"parameters": {
@@ -1904,6 +1913,9 @@
19041913
"description": "Azure Open AI Endpoint"
19051914
}
19061915
},
1916+
"azureAiAgentApiVersion": {
1917+
"type": "string"
1918+
},
19071919
"azureOpenAIApiVersion": {
19081920
"type": "string"
19091921
},
@@ -2255,7 +2267,11 @@
22552267
},
22562268
{
22572269
"name": "AZURE_AI_AGENT_API_VERSION",
2258-
"value": "[parameters('azureOpenAIApiVersion')]"
2270+
"value": "[parameters('azureAiAgentApiVersion')]"
2271+
},
2272+
{
2273+
"name": "SOLUTION_NAME",
2274+
"value": "[parameters('solutionName')]"
22592275
},
22602276
{
22612277
"name": "USE_CHAT_HISTORY_ENABLED",
Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
azure-storage-file-datalake
2-
openai
3-
pypdf
1+
azure-storage-file-datalake==12.20.0
2+
openai==1.84.0
3+
pypdf==5.6.0
44
# pyodbc
5-
tiktoken
6-
msal[broker]==1.31.1
7-
azure-identity
8-
azure-ai-textanalytics
5+
tiktoken==0.9.0
6+
msal[broker]==1.32.3
7+
azure-identity==1.23.0
8+
azure-ai-textanalytics==5.3.0
99
azure-search-documents==11.6.0b12
10-
azure-keyvault-secrets
11-
pandas
10+
azure-keyvault-secrets==4.9.0
11+
pandas==2.3.0
1212
# pymssql
1313
datetime

infra/scripts/run_create_index_scripts.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,9 +127,9 @@ if [ -n "$managedIdentityClientId" ]; then
127127
fi
128128

129129
# Determine the correct Python command
130-
if command -v python3 &> /dev/null; then
130+
if command -v python3 && python3 --version &> /dev/null; then
131131
PYTHON_CMD="python3"
132-
elif command -v python &> /dev/null; then
132+
elif command -v python && python --version &> /dev/null; then
133133
PYTHON_CMD="python"
134134
else
135135
echo "Python is not installed on this system. Or it is not added in the PATH."

src/app.py

Lines changed: 81 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,31 @@ def create_app():
6969
app.register_blueprint(bp)
7070
app.config["TEMPLATES_AUTO_RELOAD"] = True
7171
app.config['PROVIDE_AUTOMATIC_OPTIONS'] = True
72+
73+
@app.after_serving
74+
async def shutdown():
75+
"""
76+
Perform any cleanup tasks after the app stops serving requests.
77+
"""
78+
print("Shutting down the application...", flush=True)
79+
try:
80+
# Clean up agent instances
81+
await BrowseAgentFactory.delete_agent()
82+
await TemplateAgentFactory.delete_agent()
83+
await SectionAgentFactory.delete_agent()
84+
85+
# clear app state
86+
if hasattr(app, 'browse_agent') or hasattr(app, 'template_agent') or hasattr(app, 'section_agent'):
87+
app.browse_agent = None
88+
app.template_agent = None
89+
app.section_agent = None
90+
91+
track_event_if_configured("ApplicationShutdown", {"status": "success"})
92+
except Exception as e:
93+
logging.exception("Error during application shutdown")
94+
track_event_if_configured("ApplicationShutdownError", {"status": "error"})
95+
raise e
96+
7297
return app
7398

7499

@@ -248,10 +273,15 @@ async def send_chat_request(request_body, request_headers) -> AsyncGenerator[Dic
248273
run_id = None
249274
streamed_titles = set()
250275
doc_mapping = {}
276+
thread = None
251277
# Browse
252278
if request_body["chat_type"] == "browse":
253279
try:
254-
browse_agent_data = await BrowseAgentFactory.get_browse_agent(system_instruction=app_settings.azure_openai.system_message)
280+
# Create browse agent if it doesn't exist
281+
if getattr(app, "browse_agent", None) is None:
282+
app.browse_agent = await BrowseAgentFactory.get_agent()
283+
284+
browse_agent_data = app.browse_agent
255285
browse_project_client = browse_agent_data["client"]
256286
browse_agent = browse_agent_data["agent"]
257287

@@ -283,10 +313,18 @@ async def send_chat_request(request_body, request_headers) -> AsyncGenerator[Dic
283313

284314
if delta_text and delta_text.value:
285315
answer["answer"] += delta_text.value
286-
yield {
287-
"answer": convert_citation_markers(delta_text.value, doc_mapping),
288-
"citations": json.dumps(answer["citations"])
289-
}
316+
317+
# check if citation markers are present
318+
has_citation_markers = bool(re.search(r'【(\d+:\d+)†source】', delta_text.value))
319+
if has_citation_markers:
320+
yield {
321+
"answer": convert_citation_markers(delta_text.value, doc_mapping),
322+
"citations": json.dumps(answer["citations"])
323+
}
324+
else:
325+
yield {
326+
"answer": delta_text.value
327+
}
290328

291329
if delta_text and delta_text.annotations:
292330
for annotation in delta_text.annotations:
@@ -305,10 +343,11 @@ async def send_chat_request(request_body, request_headers) -> AsyncGenerator[Dic
305343
if run_id:
306344
await extract_citations_from_run_steps(browse_project_client, thread.id, run_id, answer, streamed_titles)
307345

308-
yield {
309-
# "answer": answer["answer"],
310-
"citations": json.dumps(answer["citations"])
311-
}
346+
has_final_citation_markers = bool(re.search(r'【(\d+:\d+)†source】', answer["answer"]))
347+
if has_final_citation_markers:
348+
yield {
349+
"citations": json.dumps(answer["citations"])
350+
}
312351

313352
else:
314353
run = await browse_project_client.agents.runs.create_and_process(
@@ -324,12 +363,19 @@ async def send_chat_request(request_body, request_headers) -> AsyncGenerator[Dic
324363
async for msg in messages:
325364
if msg.role == MessageRole.AGENT and msg.text_messages:
326365
answer["answer"] = msg.text_messages[-1].text.value
327-
answer["answer"] = convert_citation_markers(answer["answer"], doc_mapping)
328366
break
329-
yield {
330-
"answer": answer["answer"],
331-
"citations": json.dumps(answer["citations"])
332-
}
367+
368+
has_citation_markers = bool(re.search(r'【(\d+:\d+)†source】', answer["answer"]))
369+
370+
if has_citation_markers:
371+
yield {
372+
"answer": convert_citation_markers(answer["answer"], doc_mapping),
373+
"citations": json.dumps(answer["citations"])
374+
}
375+
else:
376+
yield {
377+
"answer": answer["answer"]
378+
}
333379
finally:
334380
if thread:
335381
print(f"Deleting browse thread: {thread.id}", flush=True)
@@ -338,7 +384,19 @@ async def send_chat_request(request_body, request_headers) -> AsyncGenerator[Dic
338384
# Generate Template
339385
else:
340386
try:
341-
template_agent_data = await TemplateAgentFactory.get_template_agent(system_instruction=app_settings.azure_openai.template_system_message)
387+
# Create template agent if it doesn't exist
388+
if getattr(app, "template_agent", None) is None:
389+
app.template_agent = await TemplateAgentFactory.get_agent()
390+
391+
# Create section_agent if missing; log errors without stopping flow
392+
try:
393+
if getattr(app, "section_agent", None) is None:
394+
app.section_agent = await SectionAgentFactory.get_agent()
395+
except Exception as e:
396+
logging.exception("Error initializing Section Agent", e)
397+
raise e
398+
399+
template_agent_data = app.template_agent
342400
template_project_client = template_agent_data["client"]
343401
template_agent = template_agent_data["agent"]
344402

@@ -1032,7 +1090,7 @@ async def ensure_cosmos():
10321090
success, err = await cosmos_conversation_client.ensure()
10331091
if not cosmos_conversation_client or not success:
10341092
if err:
1035-
track_event_if_configured("CosmosEnsureFailed", err)
1093+
track_event_if_configured("CosmosEnsureFailed", {"error": err})
10361094
return jsonify({"error": err}), 422
10371095
return jsonify({"error": "CosmosDB is not configured or not working"}), 500
10381096

@@ -1193,11 +1251,17 @@ async def get_section_content(request_body, request_headers):
11931251
messages.append({"role": "user", "content": user_prompt})
11941252

11951253
request_body["messages"] = messages
1254+
thread = None
1255+
response_text = ""
11961256

11971257
try:
11981258
# Use Foundry SDK for section content generation
11991259
track_event_if_configured("Foundry_sdk_for_section", {"status": "success"})
1200-
section_agent_data = await SectionAgentFactory.get_sections_agent(system_instruction=app_settings.azure_openai.generate_section_content_prompt)
1260+
# Create section agent if not already created
1261+
if getattr(app, "section_agent", None) is None:
1262+
app.section_agent = await SectionAgentFactory.get_agent()
1263+
1264+
section_agent_data = app.section_agent
12011265
section_project_client = section_agent_data["client"]
12021266
section_agent = section_agent_data["agent"]
12031267

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import asyncio
2+
from abc import ABC, abstractmethod
3+
from typing import Optional
4+
5+
6+
class BaseAgentFactory(ABC):
7+
"""Base factory class for creating and managing agent instances."""
8+
_lock = asyncio.Lock()
9+
_agent: Optional[object] = None
10+
11+
@classmethod
12+
async def get_agent(cls) -> object:
13+
"""Get or create an agent instance using singleton pattern."""
14+
async with cls._lock:
15+
if cls._agent is None:
16+
cls._agent = await cls.create_or_get_agent()
17+
return cls._agent
18+
19+
@classmethod
20+
async def delete_agent(cls):
21+
"""Delete the current agent instance."""
22+
async with cls._lock:
23+
if cls._agent is not None:
24+
await cls._delete_agent_instance(cls._agent)
25+
cls._agent = None
26+
27+
@classmethod
28+
@abstractmethod
29+
async def create_or_get_agent(cls) -> object:
30+
"""Create a new agent instance."""
31+
pass
32+
33+
@classmethod
34+
@abstractmethod
35+
async def _delete_agent_instance(cls, agent: object):
36+
"""Delete the specified agent instance."""
37+
pass

0 commit comments

Comments
 (0)