Skip to content

Commit 9ad050f

Browse files
authored
Don't ascii escape unicode chars in prompts and completions (Azure#40003)
* don't ascii encode unicode chars in prompts and completions
1 parent e9cb15e commit 9ad050f

15 files changed

+452
-417
lines changed

sdk/ai/azure-ai-inference/assets.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22
"AssetsRepo": "Azure/azure-sdk-assets",
33
"AssetsRepoPrefixPath": "python",
44
"TagPrefix": "python/ai/azure-ai-inference",
5-
"Tag": "python/ai/azure-ai-inference_3f06cee8a7"
5+
"Tag": "python/ai/azure-ai-inference_01cf9f82e3"
66
}

sdk/ai/azure-ai-inference/azure/ai/inference/tracing.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ def _add_request_chat_message_events(self, span: "AbstractSpan", **kwargs: Any)
208208
f"gen_ai.{message.get('role')}.message",
209209
{
210210
"gen_ai.system": _INFERENCE_GEN_AI_SYSTEM_NAME,
211-
"gen_ai.event.content": json.dumps(message),
211+
"gen_ai.event.content": json.dumps(message, ensure_ascii=False),
212212
},
213213
timestamp,
214214
)
@@ -300,7 +300,7 @@ def _add_response_chat_message_events(
300300
full_response["message"]["tool_calls"] = [tool.as_dict() for tool in choice.message.tool_calls]
301301
attributes = {
302302
"gen_ai.system": _INFERENCE_GEN_AI_SYSTEM_NAME,
303-
"gen_ai.event.content": json.dumps(full_response),
303+
"gen_ai.event.content": json.dumps(full_response, ensure_ascii=False),
304304
}
305305
else:
306306
response: Dict[str, Any] = {
@@ -318,7 +318,7 @@ def _add_response_chat_message_events(
318318

319319
attributes = {
320320
"gen_ai.system": _INFERENCE_GEN_AI_SYSTEM_NAME,
321-
"gen_ai.event.content": json.dumps(response),
321+
"gen_ai.event.content": json.dumps(response, ensure_ascii=False),
322322
}
323323
last_event_timestamp_ns = self._record_event(span, "gen_ai.choice", attributes, last_event_timestamp_ns)
324324

@@ -478,7 +478,7 @@ def __iter__( # pyright: ignore [reportIncompatibleMethodOverride]
478478
)
479479
attributes = {
480480
"gen_ai.system": _INFERENCE_GEN_AI_SYSTEM_NAME,
481-
"gen_ai.event.content": json.dumps(accumulate),
481+
"gen_ai.event.content": json.dumps(accumulate, ensure_ascii=False),
482482
}
483483
self._instrumentor._record_event(span, "gen_ai.choice", attributes, previous_event_timestamp)
484484
span.finish()
@@ -532,7 +532,7 @@ def _trace_stream_content(self) -> None:
532532
self._accumulate["message"]["tool_calls"] = list(tools_no_recording)
533533
attributes = {
534534
"gen_ai.system": _INFERENCE_GEN_AI_SYSTEM_NAME,
535-
"gen_ai.event.content": json.dumps(self._accumulate),
535+
"gen_ai.event.content": json.dumps(self._accumulate, ensure_ascii=False),
536536
}
537537
self._last_event_timestamp_ns = self._instrumentor._record_event( # pylint: disable=protected-access, line-too-long # pyright: ignore [reportFunctionMemberAccess]
538538
span, "gen_ai.choice", attributes, self._last_event_timestamp_ns

sdk/ai/azure-ai-inference/tests/test_client_tracing.py

+57-8
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
# Copyright (c) Microsoft Corporation.
44
# Licensed under the MIT License.
55
# ------------------------------------
6+
import json
67
import os
78
import azure.ai.inference as sdk
89
from azure.ai.inference.tracing import AIInferenceInstrumentor
@@ -322,6 +323,58 @@ def test_chat_completion_tracing_content_recording_enabled(self, **kwargs):
322323
assert events_match == True
323324
AIInferenceInstrumentor().uninstrument()
324325

326+
@ServicePreparerChatCompletions()
327+
@recorded_by_proxy
328+
def test_chat_completion_tracing_content_unicode(self, **kwargs):
329+
# Make sure code is not instrumented due to a previous test exception
330+
try:
331+
AIInferenceInstrumentor().uninstrument()
332+
except RuntimeError as e:
333+
pass
334+
self.modify_env_var(CONTENT_TRACING_ENV_VARIABLE, "True")
335+
client = self._create_chat_client(**kwargs)
336+
processor, exporter = self.setup_memory_trace_exporter()
337+
AIInferenceInstrumentor().instrument()
338+
response = client.complete(
339+
messages=[
340+
sdk.models.SystemMessage(content="You are a helpful assistant."),
341+
sdk.models.UserMessage(content="将“hello world”翻译成中文和乌克兰语"),
342+
],
343+
)
344+
processor.force_flush()
345+
spans = exporter.get_spans_by_name_starts_with("chat")
346+
assert len(spans) == 1
347+
expected_events = [
348+
{
349+
"name": "gen_ai.system.message",
350+
"attributes": {
351+
"gen_ai.system": "az.ai.inference",
352+
"gen_ai.event.content": '{"role": "system", "content": "You are a helpful assistant."}',
353+
},
354+
},
355+
{
356+
"name": "gen_ai.user.message",
357+
"attributes": {
358+
"gen_ai.system": "az.ai.inference",
359+
"gen_ai.event.content": '{"role": "user", "content": "将“hello world”翻译成中文和乌克兰语"}',
360+
},
361+
},
362+
{
363+
"name": "gen_ai.choice",
364+
"attributes": {
365+
"gen_ai.system": "az.ai.inference",
366+
"gen_ai.event.content": '{"message": {"content": "*"}, "finish_reason": "stop", "index": 0}',
367+
},
368+
},
369+
]
370+
events_match = GenAiTraceVerifier().check_span_events(spans[0], expected_events)
371+
assert events_match == True
372+
373+
completion_event_content = json.loads(spans[0].events[2].attributes["gen_ai.event.content"])
374+
assert False == completion_event_content["message"]["content"].isascii()
375+
assert response.choices[0].message.content == completion_event_content["message"]["content"]
376+
AIInferenceInstrumentor().uninstrument()
377+
325378
@ServicePreparerChatCompletions()
326379
@recorded_by_proxy
327380
def test_chat_completion_streaming_tracing_content_recording_disabled(self, **kwargs):
@@ -344,14 +397,12 @@ def test_chat_completion_streaming_tracing_content_recording_disabled(self, **kw
344397
)
345398
response_content = ""
346399
for update in response:
347-
if update.choices:
400+
if update.choices and update.choices[0].delta.content:
348401
response_content = response_content + update.choices[0].delta.content
349402
client.close()
350403

351404
processor.force_flush()
352-
spans = exporter.get_spans_by_name_starts_with("chat ")
353-
if len(spans) == 0:
354-
spans = exporter.get_spans_by_name("chat")
405+
spans = exporter.get_spans_by_name_starts_with("chat")
355406
assert len(spans) == 1
356407
span = spans[0]
357408
expected_attributes = [
@@ -403,7 +454,7 @@ def test_chat_completion_streaming_tracing_content_recording_enabled(self, **kwa
403454
)
404455
response_content = ""
405456
for update in response:
406-
if update.choices:
457+
if update.choices and update.choices[0].delta.content:
407458
response_content = response_content + update.choices[0].delta.content
408459
client.close()
409460

@@ -527,9 +578,7 @@ def get_weather(city: str) -> str:
527578
# With the additional tools information on hand, get another response from the model
528579
response = client.complete(messages=messages, tools=[weather_description])
529580
processor.force_flush()
530-
spans = exporter.get_spans_by_name_starts_with("chat ")
531-
if len(spans) == 0:
532-
spans = exporter.get_spans_by_name("chat")
581+
spans = exporter.get_spans_by_name_starts_with("chat")
533582
assert len(spans) == 2
534583
expected_attributes = [
535584
("gen_ai.operation.name", "chat"),

sdk/ai/azure-ai-projects/README.md

+7-5
Original file line numberDiff line numberDiff line change
@@ -580,7 +580,7 @@ agent = project_client.agents.create_agent(
580580
Currently, the Azure Function integration for the AI Agent has the following limitations:
581581

582582
- Azure Functions integration is available **only for non-streaming scenarios**.
583-
- Supported trigger for Azure Function is currently limited to **Queue triggers** only.
583+
- Supported trigger for Azure Function is currently limited to **Queue triggers** only.
584584
HTTP or other trigger types and streaming responses are not supported at this time.
585585

586586
---
@@ -601,8 +601,8 @@ app = func.FunctionApp()
601601
@app.get_weather(arg_name="inputQueue",
602602
queue_name="input",
603603
connection="AzureWebJobsStorage")
604-
@app.queue_output(arg_name="outputQueue",
605-
queue_name="output",
604+
@app.queue_output(arg_name="outputQueue",
605+
queue_name="output",
606606
connection="AzureWebJobsStorage")
607607
def get_weather(inputQueue: func.QueueMessage, outputQueue: func.Out[str]):
608608
try:
@@ -852,7 +852,7 @@ message = project_client.agents.create_message(
852852

853853
#### Create Message with Code Interpreter Attachment
854854

855-
To attach a file to a message for data analysis, use `MessageAttachment` and `CodeInterpreterTool` classes. You must pass `CodeInterpreterTool` as `tools` or `toolset` in `create_agent` call or the file attachment cannot be opened for code interpreter.
855+
To attach a file to a message for data analysis, use `MessageAttachment` and `CodeInterpreterTool` classes. You must pass `CodeInterpreterTool` as `tools` or `toolset` in `create_agent` call or the file attachment cannot be opened for code interpreter.
856856

857857
Here is an example to pass `CodeInterpreterTool` as tool:
858858

@@ -1288,12 +1288,14 @@ if not application_insights_connection_string:
12881288
exit()
12891289
configure_azure_monitor(connection_string=application_insights_connection_string)
12901290

1291+
# enable additional instrumentations
1292+
project_client.telemetry.enable()
1293+
12911294
scenario = os.path.basename(__file__)
12921295
tracer = trace.get_tracer(__name__)
12931296

12941297
with tracer.start_as_current_span(scenario):
12951298
with project_client:
1296-
project_client.telemetry.enable()
12971299
```
12981300

12991301
<!-- END SNIPPET -->

sdk/ai/azure-ai-projects/assets.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22
"AssetsRepo": "Azure/azure-sdk-assets",
33
"AssetsRepoPrefixPath": "python",
44
"TagPrefix": "python/ai/azure-ai-projects",
5-
"Tag": "python/ai/azure-ai-projects_04dc35e78c"
5+
"Tag": "python/ai/azure-ai-projects_e7dc31a23f"
66
}

sdk/ai/azure-ai-projects/azure/ai/projects/telemetry/agents/_ai_agents_instrumentor.py

+25-23
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,7 @@ def _add_message_event(
343343
message_status=message_status,
344344
usage=usage,
345345
)
346-
attributes[GEN_AI_EVENT_CONTENT] = json.dumps(event_body)
346+
attributes[GEN_AI_EVENT_CONTENT] = json.dumps(event_body, ensure_ascii=False)
347347
span.span_instance.add_event(name=f"gen_ai.{role}.message", attributes=attributes)
348348

349349
def _get_field(self, obj: Any, field: str) -> Any:
@@ -374,7 +374,7 @@ def _add_instructions_event(
374374
event_body["content"] = instructions or additional_instructions
375375

376376
attributes = self._create_event_attributes(agent_id=agent_id, thread_id=thread_id)
377-
attributes[GEN_AI_EVENT_CONTENT] = json.dumps(event_body)
377+
attributes[GEN_AI_EVENT_CONTENT] = json.dumps(event_body, ensure_ascii=False)
378378
span.span_instance.add_event(name=GEN_AI_SYSTEM_MESSAGE, attributes=attributes)
379379

380380
def _get_role(self, role: Optional[Union[str, MessageRole]]) -> str:
@@ -413,10 +413,10 @@ def _add_tool_assistant_message_event(self, span, step: RunStep) -> None:
413413
)
414414

415415
if _trace_agents_content:
416-
attributes[GEN_AI_EVENT_CONTENT] = json.dumps({"tool_calls": tool_calls})
416+
attributes[GEN_AI_EVENT_CONTENT] = json.dumps({"tool_calls": tool_calls}, ensure_ascii=False)
417417
else:
418418
tool_calls_non_recording = self._remove_function_call_names_and_arguments(tool_calls=tool_calls)
419-
attributes[GEN_AI_EVENT_CONTENT] = json.dumps({"tool_calls": tool_calls_non_recording})
419+
attributes[GEN_AI_EVENT_CONTENT] = json.dumps({"tool_calls": tool_calls_non_recording}, ensure_ascii=False)
420420
span.span_instance.add_event(name="gen_ai.assistant.message", attributes=attributes)
421421

422422
def set_end_run(self, span: "AbstractSpan", run: Optional[ThreadRun]) -> None:
@@ -518,7 +518,9 @@ def _add_tool_message_events(
518518
body = {"content": tool_output["output"], "id": tool_output["tool_call_id"]}
519519
else:
520520
body = {"content": "", "id": tool_output["tool_call_id"]}
521-
span.span_instance.add_event("gen_ai.tool.message", {"gen_ai.event.content": json.dumps(body)})
521+
span.span_instance.add_event(
522+
"gen_ai.tool.message", {"gen_ai.event.content": json.dumps(body, ensure_ascii=False)}
523+
)
522524
return True
523525

524526
return False
@@ -1330,33 +1332,33 @@ def inner(*args, **kwargs): # pylint: disable=R0911
13301332
class_function_name = function.__qualname__
13311333

13321334
if class_function_name.startswith("AgentsOperations.create_agent"):
1333-
kwargs.setdefault('merge_span', True)
1335+
kwargs.setdefault("merge_span", True)
13341336
return self.trace_create_agent(function, *args, **kwargs)
13351337
if class_function_name.startswith("AgentsOperations.create_thread"):
1336-
kwargs.setdefault('merge_span', True)
1338+
kwargs.setdefault("merge_span", True)
13371339
return self.trace_create_thread(function, *args, **kwargs)
13381340
if class_function_name.startswith("AgentsOperations.create_message"):
1339-
kwargs.setdefault('merge_span', True)
1341+
kwargs.setdefault("merge_span", True)
13401342
return self.trace_create_message(function, *args, **kwargs)
13411343
if class_function_name.startswith("AgentsOperations.create_run"):
1342-
kwargs.setdefault('merge_span', True)
1344+
kwargs.setdefault("merge_span", True)
13431345
return self.trace_create_run(OperationName.START_THREAD_RUN, function, *args, **kwargs)
13441346
if class_function_name.startswith("AgentsOperations.create_and_process_run"):
1345-
kwargs.setdefault('merge_span', True)
1347+
kwargs.setdefault("merge_span", True)
13461348
return self.trace_create_run(OperationName.PROCESS_THREAD_RUN, function, *args, **kwargs)
13471349
if class_function_name.startswith("AgentsOperations.submit_tool_outputs_to_run"):
1348-
kwargs.setdefault('merge_span', True)
1350+
kwargs.setdefault("merge_span", True)
13491351
return self.trace_submit_tool_outputs(False, function, *args, **kwargs)
13501352
if class_function_name.startswith("AgentsOperations.submit_tool_outputs_to_stream"):
1351-
kwargs.setdefault('merge_span', True)
1353+
kwargs.setdefault("merge_span", True)
13521354
return self.trace_submit_tool_outputs(True, function, *args, **kwargs)
13531355
if class_function_name.startswith("AgentsOperations._handle_submit_tool_outputs"):
13541356
return self.trace_handle_submit_tool_outputs(function, *args, **kwargs)
13551357
if class_function_name.startswith("AgentsOperations.create_stream"):
1356-
kwargs.setdefault('merge_span', True)
1358+
kwargs.setdefault("merge_span", True)
13571359
return self.trace_create_stream(function, *args, **kwargs)
13581360
if class_function_name.startswith("AgentsOperations.list_messages"):
1359-
kwargs.setdefault('merge_span', True)
1361+
kwargs.setdefault("merge_span", True)
13601362
return self.trace_list_messages(function, *args, **kwargs)
13611363
if class_function_name.startswith("AgentRunStream.__exit__"):
13621364
return self.handle_run_stream_exit(function, *args, **kwargs)
@@ -1398,33 +1400,33 @@ async def inner(*args, **kwargs): # pylint: disable=R0911
13981400
class_function_name = function.__qualname__
13991401

14001402
if class_function_name.startswith("AgentsOperations.create_agent"):
1401-
kwargs.setdefault('merge_span', True)
1403+
kwargs.setdefault("merge_span", True)
14021404
return await self.trace_create_agent_async(function, *args, **kwargs)
14031405
if class_function_name.startswith("AgentsOperations.create_thread"):
1404-
kwargs.setdefault('merge_span', True)
1406+
kwargs.setdefault("merge_span", True)
14051407
return await self.trace_create_thread_async(function, *args, **kwargs)
14061408
if class_function_name.startswith("AgentsOperations.create_message"):
1407-
kwargs.setdefault('merge_span', True)
1409+
kwargs.setdefault("merge_span", True)
14081410
return await self.trace_create_message_async(function, *args, **kwargs)
14091411
if class_function_name.startswith("AgentsOperations.create_run"):
1410-
kwargs.setdefault('merge_span', True)
1412+
kwargs.setdefault("merge_span", True)
14111413
return await self.trace_create_run_async(OperationName.START_THREAD_RUN, function, *args, **kwargs)
14121414
if class_function_name.startswith("AgentsOperations.create_and_process_run"):
1413-
kwargs.setdefault('merge_span', True)
1415+
kwargs.setdefault("merge_span", True)
14141416
return await self.trace_create_run_async(OperationName.PROCESS_THREAD_RUN, function, *args, **kwargs)
14151417
if class_function_name.startswith("AgentsOperations.submit_tool_outputs_to_run"):
1416-
kwargs.setdefault('merge_span', True)
1418+
kwargs.setdefault("merge_span", True)
14171419
return await self.trace_submit_tool_outputs_async(False, function, *args, **kwargs)
14181420
if class_function_name.startswith("AgentsOperations.submit_tool_outputs_to_stream"):
1419-
kwargs.setdefault('merge_span', True)
1421+
kwargs.setdefault("merge_span", True)
14201422
return await self.trace_submit_tool_outputs_async(True, function, *args, **kwargs)
14211423
if class_function_name.startswith("AgentsOperations._handle_submit_tool_outputs"):
14221424
return await self.trace_handle_submit_tool_outputs_async(function, *args, **kwargs)
14231425
if class_function_name.startswith("AgentsOperations.create_stream"):
1424-
kwargs.setdefault('merge_span', True)
1426+
kwargs.setdefault("merge_span", True)
14251427
return await self.trace_create_stream_async(function, *args, **kwargs)
14261428
if class_function_name.startswith("AgentsOperations.list_messages"):
1427-
kwargs.setdefault('merge_span', True)
1429+
kwargs.setdefault("merge_span", True)
14281430
return await self.trace_list_messages_async(function, *args, **kwargs)
14291431
if class_function_name.startswith("AsyncAgentRunStream.__aexit__"):
14301432
return self.handle_run_stream_exit(function, *args, **kwargs)

sdk/ai/azure-ai-projects/samples/agents/async_samples/sample_agents_basics_async_with_azure_monitor_tracing.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
scenario = os.path.basename(__file__)
3333
tracer = trace.get_tracer(__name__)
3434

35+
3536
async def main() -> None:
3637

3738
async with DefaultAzureCredential() as creds:
@@ -47,11 +48,15 @@ async def main() -> None:
4748
exit()
4849
configure_azure_monitor(connection_string=application_insights_connection_string)
4950

51+
# enable additional instrumentations
52+
project_client.telemetry.enable()
53+
5054
with tracer.start_as_current_span(scenario):
5155
async with project_client:
52-
project_client.telemetry.enable()
5356
agent = await project_client.agents.create_agent(
54-
model=os.environ["MODEL_DEPLOYMENT_NAME"], name="my-assistant", instructions="You are helpful assistant"
57+
model=os.environ["MODEL_DEPLOYMENT_NAME"],
58+
name="my-assistant",
59+
instructions="You are helpful assistant",
5560
)
5661
print(f"Created agent, agent ID: {agent.id}")
5762

0 commit comments

Comments
 (0)