Skip to content

Commit f473a92

Browse files
fix: upgrade agent framework versions
2 parents 742a0a7 + e45bce4 commit f473a92

5 files changed

Lines changed: 181 additions & 126 deletions

File tree

infra/vscode_web/requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
azure-ai-projects==2.0.0b3
1+
azure-ai-projects==2.1.0
22
azure-identity==1.20.0
33
ansible-core~=2.17.0

src/backend/orchestrator.py

Lines changed: 79 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,12 @@
2222
from typing import AsyncIterator, Optional, cast
2323

2424
from agent_framework import (
25-
ChatMessage,
26-
HandoffBuilder,
27-
HandoffAgentUserRequest,
28-
RequestInfoEvent,
29-
WorkflowOutputEvent,
30-
WorkflowStatusEvent,
25+
Agent,
26+
Message,
27+
WorkflowEventType,
3128
)
32-
from agent_framework.azure import AzureOpenAIChatClient
29+
from agent_framework.orchestrations import HandoffBuilder, HandoffAgentUserRequest
30+
from agent_framework.openai import OpenAIChatCompletionClient
3331
from azure.identity import DefaultAzureCredential
3432

3533
# Foundry imports - only used when USE_FOUNDRY=true
@@ -48,6 +46,11 @@
4846
# Token endpoint for Azure Cognitive Services (used for Azure OpenAI)
4947
TOKEN_ENDPOINT = "https://cognitiveservices.azure.com/.default"
5048

49+
# Event type constants for type-safe dispatch (avoids string typos)
50+
EVENT_STATUS: WorkflowEventType = "status"
51+
EVENT_REQUEST_INFO: WorkflowEventType = "request_info"
52+
EVENT_OUTPUT: WorkflowEventType = "output"
53+
5154

5255
# Harmful content patterns to detect in USER INPUT before processing
5356
# This provides proactive content safety by blocking harmful requests at the input layer
@@ -120,9 +123,9 @@ def _check_input_for_harmful_content(message: str) -> tuple[bool, str]:
120123
r"You are a Text Content Agent",
121124
r"You are an Image Content Agent",
122125
r"You are a Compliance Agent",
123-
# Handoff instructions
124-
r"hand off to \w+_agent",
125-
r"hand back to \w+_agent",
126+
# Handoff instructions (match both underscore and hyphen agent names)
127+
r"hand off to \w+[_\-]agent",
128+
r"hand back to \w+[_\-]agent",
126129
r"may hand off to",
127130
r"After (?:generating|completing|validation|parsing)",
128131
# Internal workflow markers
@@ -139,8 +142,8 @@ def _check_input_for_harmful_content(message: str) -> tuple[bool, str]:
139142
# RAI internal instructions
140143
r"NEVER generate images that contain:",
141144
r"Responsible AI - Image Generation Rules",
142-
# Agent framework references
143-
r"compliance_agent|triage_agent|planning_agent|research_agent|text_content_agent|image_content_agent",
145+
# Agent framework references (match both underscore and hyphen separators)
146+
r"compliance[_\-]agent|triage[_\-]agent|planning[_\-]agent|research[_\-]agent|text[_\-]content[_\-]agent|image[_\-]content[_\-]agent",
144147
]
145148

146149
_SYSTEM_PROMPT_PATTERNS_COMPILED = [re.compile(pattern, re.IGNORECASE | re.DOTALL) for pattern in SYSTEM_PROMPT_PATTERNS]
@@ -485,7 +488,7 @@ class ContentGenerationOrchestrator:
485488
Microsoft Agent Framework's HandoffBuilder.
486489
487490
Supports two modes:
488-
1. Azure OpenAI Direct (default): Uses AzureOpenAIChatClient with ad_token_provider
491+
1. Azure OpenAI Direct (default): Uses OpenAIChatCompletionClient with DefaultAzureCredential
489492
2. Azure AI Foundry: Uses AIProjectClient with project endpoint (set USE_FOUNDRY=true)
490493
491494
Agents:
@@ -498,7 +501,7 @@ class ContentGenerationOrchestrator:
498501
"""
499502

500503
def __init__(self):
501-
self._chat_client = None # Always AzureOpenAIChatClient
504+
self._chat_client = None # OpenAIChatCompletionClient instance
502505
self._project_client = None # AIProjectClient for Foundry mode (used for image generation)
503506
self._agents: dict = {}
504507
self._rai_agent = None
@@ -536,7 +539,6 @@ def _get_chat_client(self):
536539
# Store the project client for image generation
537540
self._project_client = project_client
538541

539-
# For chat completions, use the direct Azure OpenAI endpoint
540542
# The Foundry project uses Azure OpenAI under the hood, and we need the direct endpoint
541543
# to properly authenticate with Cognitive Services token
542544
azure_endpoint = app_settings.azure_openai.endpoint
@@ -552,11 +554,11 @@ def get_token() -> str:
552554
api_version = app_settings.azure_openai.api_version
553555

554556
logger.info(f"Foundry mode using Azure OpenAI endpoint: {azure_endpoint}, deployment: {model_deployment}")
555-
self._chat_client = AzureOpenAIChatClient(
556-
endpoint=azure_endpoint,
557-
deployment_name=model_deployment,
557+
self._chat_client = OpenAIChatCompletionClient(
558+
azure_endpoint=azure_endpoint,
559+
model=model_deployment,
558560
api_version=api_version,
559-
ad_token_provider=get_token,
561+
credential=get_token,
560562
)
561563
else:
562564
# Azure OpenAI Direct mode
@@ -569,12 +571,12 @@ def get_token() -> str:
569571
token = self._credential.get_token(TOKEN_ENDPOINT)
570572
return token.token
571573

572-
logger.info("Using Azure OpenAI Direct mode with ad_token_provider")
573-
self._chat_client = AzureOpenAIChatClient(
574-
endpoint=endpoint,
575-
deployment_name=app_settings.azure_openai.gpt_model,
574+
logger.info("Using Azure OpenAI Direct mode with DefaultAzureCredential token provider")
575+
self._chat_client = OpenAIChatCompletionClient(
576+
azure_endpoint=endpoint,
577+
model=app_settings.azure_openai.gpt_model,
576578
api_version=app_settings.azure_openai.api_version,
577-
ad_token_provider=get_token,
579+
credential=get_token,
578580
)
579581
return self._chat_client
580582

@@ -589,40 +591,60 @@ def initialize(self) -> None:
589591
# Get the chat client
590592
chat_client = self._get_chat_client()
591593

592-
# Agent names - use underscores (AzureOpenAIChatClient works with both modes now)
594+
# Agent names - always use underscores so that instruction strings
595+
# (TRIAGE_INSTRUCTIONS, *_CONTENT_INSTRUCTIONS, etc.) and the
596+
# SYSTEM_PROMPT_PATTERNS leakage-detection regexes stay in sync.
597+
# Foundry workflows accept underscore names; no hyphen conversion needed.
593598
name_sep = "_"
594599

595600
# Create all agents
596-
triage_agent = chat_client.create_agent(
601+
# NOTE: Handoff workflow participants must set
602+
# require_per_service_call_history_persistence=True so local conversation
603+
# history stays consistent with the service across handoff tool-call
604+
# short-circuits (required by agent_framework.orchestrations.HandoffBuilder).
605+
triage_agent = Agent(
606+
client=chat_client,
597607
name=f"triage{name_sep}agent",
598608
instructions=TRIAGE_INSTRUCTIONS,
609+
require_per_service_call_history_persistence=True,
599610
)
600611

601-
planning_agent = chat_client.create_agent(
612+
planning_agent = Agent(
613+
client=chat_client,
602614
name=f"planning{name_sep}agent",
603615
instructions=PLANNING_INSTRUCTIONS,
616+
require_per_service_call_history_persistence=True,
604617
)
605618

606-
research_agent = chat_client.create_agent(
619+
research_agent = Agent(
620+
client=chat_client,
607621
name=f"research{name_sep}agent",
608622
instructions=RESEARCH_INSTRUCTIONS,
623+
require_per_service_call_history_persistence=True,
609624
)
610625

611-
text_content_agent = chat_client.create_agent(
626+
text_content_agent = Agent(
627+
client=chat_client,
612628
name=f"text{name_sep}content{name_sep}agent",
613629
instructions=TEXT_CONTENT_INSTRUCTIONS,
630+
require_per_service_call_history_persistence=True,
614631
)
615632

616-
image_content_agent = chat_client.create_agent(
633+
image_content_agent = Agent(
634+
client=chat_client,
617635
name=f"image{name_sep}content{name_sep}agent",
618636
instructions=IMAGE_CONTENT_INSTRUCTIONS,
637+
require_per_service_call_history_persistence=True,
619638
)
620639

621-
compliance_agent = chat_client.create_agent(
640+
compliance_agent = Agent(
641+
client=chat_client,
622642
name=f"compliance{name_sep}agent",
623643
instructions=COMPLIANCE_INSTRUCTIONS,
644+
require_per_service_call_history_persistence=True,
624645
)
625-
self._rai_agent = chat_client.create_agent(
646+
self._rai_agent = Agent(
647+
client=chat_client,
626648
name=f"rai{name_sep}agent",
627649
instructions=RAI_INSTRUCTIONS,
628650
)
@@ -636,7 +658,7 @@ def initialize(self) -> None:
636658
"compliance": compliance_agent,
637659
}
638660

639-
# Workflow name - Foundry requires hyphens
661+
# Workflow name
640662
workflow_name = f"content{name_sep}generation{name_sep}workflow"
641663

642664
# Build the handoff workflow
@@ -736,35 +758,35 @@ async def process_message(
736758
events.append(event)
737759

738760
# Handle different event types from the workflow
739-
if isinstance(event, WorkflowStatusEvent):
761+
if event.type == EVENT_STATUS:
762+
status_name = event.state.name if event.state else str(event.data)
740763
yield {
741764
"type": "status",
742-
"content": event.state.name,
765+
"content": status_name,
743766
"is_final": False,
744767
"metadata": {"conversation_id": conversation_id}
745768
}
746769

747-
elif isinstance(event, RequestInfoEvent):
770+
elif event.type == EVENT_REQUEST_INFO:
748771
# Workflow is requesting user input
749772
if isinstance(event.data, HandoffAgentUserRequest):
750-
# Extract conversation history from agent_response.messages (updated API)
751-
messages = event.data.agent_response.messages if hasattr(event.data, 'agent_response') and event.data.agent_response else []
752-
if not isinstance(messages, list):
753-
messages = [messages] if messages else []
773+
# Extract conversation history from agent_response.messages
774+
agent_resp = event.data.agent_response
775+
messages = list(agent_resp.messages) if agent_resp and agent_resp.messages else []
754776

755777
conversation_text = "\n".join([
756778
f"{msg.author_name or msg.role.value}: {msg.text}"
757779
for msg in messages
758780
])
759781

760782
# Get the last message content and filter any system prompt leakage
761-
last_msg_content = messages[-1].text if messages else (event.data.agent_response.text if hasattr(event.data, 'agent_response') and event.data.agent_response else "")
783+
last_msg_content = messages[-1].text if messages else (agent_resp.text if agent_resp else "")
762784
last_msg_content = _filter_system_prompt_from_response(last_msg_content)
763-
last_msg_agent = messages[-1].author_name if messages and hasattr(messages[-1], 'author_name') else "unknown"
785+
last_msg_agent = messages[-1].author_name if messages else "unknown"
764786

765787
yield {
766788
"type": "agent_response",
767-
"agent": last_msg_agent,
789+
"agent": last_msg_agent or "unknown",
768790
"content": last_msg_content,
769791
"conversation_history": conversation_text,
770792
"is_final": False,
@@ -773,9 +795,8 @@ async def process_message(
773795
"metadata": {"conversation_id": conversation_id}
774796
}
775797

776-
elif isinstance(event, WorkflowOutputEvent):
777-
# Final output from the workflow
778-
conversation = cast(list[ChatMessage], event.data)
798+
elif event.type == EVENT_OUTPUT:
799+
conversation = cast(list[Message], event.data)
779800
if isinstance(conversation, list) and conversation:
780801
# Get the last assistant message as the final response
781802
assistant_messages = [
@@ -841,38 +862,38 @@ async def send_user_response(
841862
try:
842863
responses = {request_id: user_response}
843864
async for event in self._workflow.send_responses_streaming(responses):
844-
if isinstance(event, WorkflowStatusEvent):
865+
if event.type == EVENT_STATUS:
866+
status_name = event.state.name if event.state else str(event.data)
845867
yield {
846868
"type": "status",
847-
"content": event.state.name,
869+
"content": status_name,
848870
"is_final": False,
849871
"metadata": {"conversation_id": conversation_id}
850872
}
851873

852-
elif isinstance(event, RequestInfoEvent):
874+
elif event.type == EVENT_REQUEST_INFO:
853875
if isinstance(event.data, HandoffAgentUserRequest):
854-
# Get messages from agent_response (updated API)
855-
messages = event.data.agent_response.messages if hasattr(event.data, 'agent_response') and event.data.agent_response else []
856-
if not isinstance(messages, list):
857-
messages = [messages] if messages else []
876+
# Get messages from agent_response
877+
agent_resp = event.data.agent_response
878+
messages = list(agent_resp.messages) if agent_resp and agent_resp.messages else []
858879

859880
# Get the last message content and filter any system prompt leakage
860-
last_msg_content = messages[-1].text if messages else (event.data.agent_response.text if hasattr(event.data, 'agent_response') and event.data.agent_response else "")
881+
last_msg_content = messages[-1].text if messages else (agent_resp.text if agent_resp else "")
861882
last_msg_content = _filter_system_prompt_from_response(last_msg_content)
862-
last_msg_agent = messages[-1].author_name if messages and hasattr(messages[-1], 'author_name') else "unknown"
883+
last_msg_agent = messages[-1].author_name if messages else "unknown"
863884

864885
yield {
865886
"type": "agent_response",
866-
"agent": last_msg_agent,
887+
"agent": last_msg_agent or "unknown",
867888
"content": last_msg_content,
868889
"is_final": False,
869890
"requires_user_input": True,
870891
"request_id": event.request_id,
871892
"metadata": {"conversation_id": conversation_id}
872893
}
873894

874-
elif isinstance(event, WorkflowOutputEvent):
875-
conversation = cast(list[ChatMessage], event.data)
895+
elif event.type == EVENT_OUTPUT:
896+
conversation = cast(list[Message], event.data)
876897
if isinstance(conversation, list) and conversation:
877898
assistant_messages = [
878899
msg for msg in conversation

src/backend/requirements.txt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ quart-cors>=0.7.0
66
hypercorn>=0.17.0
77

88
# Microsoft Agent Framework
9-
agent-framework-azure-ai==1.0.0b260114
10-
agent-framework-core==1.0.0b260114
9+
agent-framework-foundry==1.1.1
10+
agent-framework-core==1.1.1
11+
agent-framework-orchestrations==1.0.0b260421
1112

1213
# OpenTelemetry (required by agent-framework)
1314
opentelemetry-semantic-conventions-ai==0.4.13
@@ -18,7 +19,7 @@ azure-cosmos>=4.7.0
1819
azure-storage-blob>=12.22.0
1920
azure-search-documents>=11.4.0
2021
azure-ai-contentsafety>=1.0.0
21-
azure-ai-projects==2.0.0b3 # Azure AI Foundry SDK (optional, for USE_FOUNDRY=true)
22+
azure-ai-projects==2.1.0 # Azure AI Foundry SDK (optional, for USE_FOUNDRY=true)
2223

2324
# OpenAI
2425
openai>=1.45.0

src/backend/services/title_service.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
import re
1010
from typing import Optional
1111

12-
from agent_framework.azure import AzureOpenAIChatClient
12+
from agent_framework import Agent
13+
from agent_framework.openai import OpenAIChatCompletionClient
1314
from azure.identity import DefaultAzureCredential
1415

1516
from settings import app_settings
@@ -63,14 +64,15 @@ def get_token() -> str:
6364
token = self._credential.get_token(TOKEN_ENDPOINT)
6465
return token.token
6566

66-
chat_client = AzureOpenAIChatClient(
67-
endpoint=endpoint,
68-
deployment_name=deployment,
67+
chat_client = OpenAIChatCompletionClient(
68+
azure_endpoint=endpoint,
69+
model=deployment,
6970
api_version=api_version,
70-
ad_token_provider=get_token,
71+
credential=get_token,
7172
)
7273

73-
self._agent = chat_client.create_agent(
74+
self._agent = Agent(
75+
client=chat_client,
7476
name="title_agent",
7577
instructions=TITLE_INSTRUCTIONS,
7678
)

0 commit comments

Comments
 (0)