55import uuid
66import re
77import ast
8- import requests , asyncio
8+ import requests
9+ import asyncio
910from typing import Dict , Any , AsyncGenerator
1011
1112from azure .core .credentials import AzureKeyCredential
3132from opentelemetry .trace import Status , StatusCode
3233from azure .ai .projects .aio import AIProjectClient
3334from azure .ai .agents .models import (
34- AzureAISearchQueryType ,
35+ AzureAISearchQueryType ,
3536 AzureAISearchTool ,
36- AgentEventHandler ,
3737 MessageRole ,
3838 RunStepToolCallDetails ,
3939 MessageDeltaChunk ,
40- RunStep ,
41- ThreadMessage ,
4240 ThreadRun ,
43- AgentStreamEvent ,
44- RunStepDeltaChunk ,
4541 MessageDeltaTextContent ,
4642 MessageDeltaTextUrlCitationAnnotation
47- )
43+ )
4844
4945bp = Blueprint ("routes" , __name__ , static_folder = "static" , template_folder = "static" )
5046
@@ -237,6 +233,7 @@ async def init_ai_foundry_client():
237233 ai_foundry_client = None
238234 raise e
239235
236+
240237async def setup_agent_thread_with_search (user_messages : list , system_instruction : str , field_mapping : dict ):
241238 project_client = AIProjectClient (
242239 endpoint = app_settings .azure_ai .agent_endpoint ,
@@ -245,12 +242,11 @@ async def setup_agent_thread_with_search(user_messages: list, system_instruction
245242 )
246243
247244 print (f"Project Client: { project_client } " , flush = True )
248-
249- print (f"Creating project index..." , flush = True )
245+ print ("Creating project index..." , flush = True )
250246 print (f"Datasource connection name: { app_settings .datasource .connection_name } " , flush = True )
251247 print (f"Index name: { app_settings .datasource .index } " , flush = True )
252248 project_index = await project_client .indexes .create_or_update (
253- name = f"project-index-{ app_settings .datasource .connection_name } -{ app_settings .datasource .index } " ,
249+ name = f"project-index-{ app_settings .datasource .connection_name } -{ app_settings .datasource .index } " ,
254250 version = "1" ,
255251 body = {
256252 "connectionName" : app_settings .datasource .connection_name ,
@@ -286,14 +282,14 @@ async def setup_agent_thread_with_search(user_messages: list, system_instruction
286282 for msg in user_messages :
287283 if not msg or "role" not in msg or "content" not in msg :
288284 continue # skip malformed messages
289-
285+
290286 if msg ["role" ] != "tool" :
291287 await project_client .agents .messages .create (
292288 thread_id = thread .id ,
293289 role = msg ["role" ],
294290 content = msg ["content" ],
295291 )
296-
292+
297293 # print("Messages in thread after creation:")
298294 # messages = project_client.agents.messages.list(thread_id=thread.id)
299295
@@ -304,6 +300,7 @@ async def setup_agent_thread_with_search(user_messages: list, system_instruction
304300
305301 return project_client , agent , thread
306302
303+
307304def init_ai_search_client ():
308305 client = None
309306
@@ -461,6 +458,7 @@ def prepare_model_args(request_body, request_headers):
461458
462459 return model_args
463460
461+
464462# Conversion of citation markers
465463def convert_citation_markers (text , doc_mapping ):
466464 def replace_marker (match ):
@@ -471,9 +469,11 @@ def replace_marker(match):
471469
472470 return re .sub (r'【(\d+:\d+)†source】' , replace_marker , text )
473471
472+
473+ # Extract citations from run steps
474474async def extract_citations_from_run_steps (project_client , thread_id , run_id , answer , streamed_titles = None ):
475475 streamed_titles = streamed_titles or set ()
476-
476+
477477 async for run_step in project_client .agents .run_steps .list (thread_id = thread_id , run_id = run_id ):
478478 if isinstance (run_step .step_details , RunStepToolCallDetails ):
479479 for tool_call in run_step .step_details .tool_calls :
@@ -509,12 +509,12 @@ async def send_chat_request(request_body, request_headers) -> AsyncGenerator[Dic
509509 request_body ["messages" ] = filtered_messages
510510 model_args = prepare_model_args (request_body , request_headers )
511511 chat_type = (
512- ChatType .BROWSE
513- if not (
514- request_body ["chat_type" ] and request_body ["chat_type" ] == "template"
515- )
516- else ChatType .TEMPLATE
512+ ChatType .BROWSE
513+ if not (
514+ request_body ["chat_type" ] and request_body ["chat_type" ] == "template"
517515 )
516+ else ChatType .TEMPLATE
517+ )
518518
519519 try :
520520 if app_settings .base_settings .use_ai_foundry_sdk :
@@ -616,7 +616,6 @@ async def send_chat_request(request_body, request_headers) -> AsyncGenerator[Dic
616616 except Exception as cleanup_error :
617617 print (f"Failed to clean up agent: { cleanup_error } " , flush = True )
618618
619-
620619 else :
621620 # Use Azure Open AI client for response
622621 track_event_if_configured ("Openai_sdk_for_response" , {"status" : "success" })
@@ -642,18 +641,18 @@ async def send_chat_request(request_body, request_headers) -> AsyncGenerator[Dic
642641 span .set_status (Status (StatusCode .ERROR , str (e )))
643642 raise e
644643
644+
645645async def complete_chat_request (request_body , request_headers ):
646646 # response, apim_request_id = await send_chat_request(request_body, request_headers)
647647 response = None
648648 history_metadata = request_body .get ("history_metadata" , {})
649-
649+
650650 async for chunk in send_chat_request (request_body , request_headers ):
651651 response = chunk # Only the last chunk matters for non-streaming
652652
653653 return format_non_streaming_response (response , history_metadata )
654654
655655
656-
657656async def stream_chat_request (request_body , request_headers ):
658657 track_event_if_configured ("StreamChatRequestStart" , {
659658 "has_history_metadata" : "history_metadata" in request_body
@@ -1362,22 +1361,24 @@ async def get_document(filepath):
13621361 span .set_status (Status (StatusCode .ERROR , str (e )))
13631362 return jsonify ({"error" : str (e )}), 500
13641363
1364+
1365+ # Fetch content from Azure Search API
13651366@bp .route ("/fetch-azure-search-content" , methods = ["POST" ])
13661367async def fetch_azure_search_content ():
13671368 try :
13681369 print ("Fetching content from Azure Search" )
13691370 request_json = await request .get_json ()
13701371 url = request_json .get ("url" )
1371-
1372+
13721373 if not url :
13731374 track_event_if_configured ("FetchAzureSearchContentFailed" , {"error" : "URL is required" })
13741375 return jsonify ({"error" : "URL is required" }), 400
1375-
1376- #Get Azure AD token
1376+
1377+ # Get Azure AD token
13771378 credential = DefaultAzureCredentialSync ()
13781379 token = credential .get_token ("https://search.azure.com/.default" )
13791380 access_token = token .token
1380-
1381+
13811382 def fetch_content ():
13821383 try :
13831384 response = requests .get (
@@ -1393,15 +1394,15 @@ def fetch_content():
13931394 content = data .get ("content" , "" )
13941395 return content
13951396 else :
1396- return
1397+ return
13971398 except Exception as e :
13981399 logging .exception ("Error fetching content from Azure Search" )
13991400 print (f"Error fetching content from Azure Search: { str (e )} " )
14001401 return f"Error: { str (e )} "
1401-
1402+
14021403 content = await asyncio .to_thread (fetch_content )
14031404 return jsonify ({"content" : content }), 200
1404-
1405+
14051406 except Exception as e :
14061407 logging .exception ("Exception in /fetch-azure-search-content" )
14071408 return jsonify ({"error" : str (e )}), 500
@@ -1458,7 +1459,7 @@ async def generate_title(conversation_messages):
14581459 return messages [- 2 ]["content" ]
14591460
14601461
1461- async def get_section_content (request_body , request_headers ):
1462+ async def get_section_content (request_body , request_headers ):
14621463 user_prompt = f"""sectionTitle: { request_body ['sectionTitle' ]}
14631464 sectionDescription: { request_body ['sectionDescription' ]}
14641465 """
@@ -1485,20 +1486,20 @@ async def get_section_content(request_body, request_headers):
14851486 print ("Draft system prompt" )
14861487 print (app_settings .azure_openai .generate_section_content_prompt )
14871488 field_mapping = {
1488- "contentFields" : ["content" ],
1489- "urlField" : "sourceurl" , # make sure your index has this field
1490- "titleField" : "sourceurl" , # fallback to filename if needed
1491- }
1489+ "contentFields" : ["content" ],
1490+ "urlField" : "sourceurl" ,
1491+ "titleField" : "sourceurl" ,
1492+ }
14921493 project_client , agent , thread = await setup_agent_thread_with_search (
14931494 user_messages = request_body ["messages" ],
14941495 system_instruction = app_settings .azure_openai .generate_section_content_prompt ,
14951496 field_mapping = field_mapping
14961497 )
14971498 run = await project_client .agents .runs .create_and_process (
1498- thread_id = thread .id ,
1499- agent_id = agent .id ,
1500- tool_choice = {"type" : "azure_ai_search" }
1501- )
1499+ thread_id = thread .id ,
1500+ agent_id = agent .id ,
1501+ tool_choice = {"type" : "azure_ai_search" }
1502+ )
15021503 if run .status == "failed" :
15031504 print (f"Run failed: { run .error_message } " )
15041505 raise Exception (f"Run failed: { run .error_message } " )
@@ -1531,15 +1532,14 @@ async def get_section_content(request_body, request_headers):
15311532 span .record_exception (e )
15321533 span .set_status (Status (StatusCode .ERROR , str (e )))
15331534 raise e
1534-
1535+
15351536 finally :
15361537 if agent :
15371538 try :
15381539 await project_client .agents .delete_agent (agent .id )
15391540 print (f"Agent deleted: { agent .id } " , flush = True )
15401541 except Exception as cleanup_error :
15411542 print (f"Failed to clean up agent: { cleanup_error } " , flush = True )
1542-
15431543
15441544 return response_text
15451545
0 commit comments