Skip to content

Commit 9bd1ef6

Browse files
committed
WIP - with new FF lib
1 parent 51d946b commit 9bd1ef6

6 files changed

Lines changed: 155 additions & 40 deletions

File tree

.config/feature-flags.json

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,39 @@
22
"schemaVersion": "2.0.0",
33
"feature_management": {
44
"feature_flags": [
5+
{
6+
"id": "eval_sampling",
7+
"enabled": true,
8+
"variants": [
9+
{
10+
"name": "On",
11+
"configuration_value": true
12+
},
13+
{
14+
"name": "Off",
15+
"configuration_value": false
16+
}
17+
],
18+
"allocation": {
19+
"percentile": [
20+
{
21+
"variant": "On",
22+
"from": 0,
23+
"to": 10
24+
},
25+
{
26+
"variant": "Off",
27+
"from": 50,
28+
"to": 90
29+
}
30+
],
31+
"default_when_enabled": "Off",
32+
"default_when_disabled": "Off"
33+
},
34+
"telemetry": {
35+
"enabled": true
36+
}
37+
},
538
{
639
"id": "prompt_asset",
740
"enabled": true,
27.4 KB
Binary file not shown.

eval/setup-eval.py

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,13 @@
1515
# Name of your online evaluation schedule
1616
SAMPLE_NAME = "online_eval_name"
1717

18-
19-
2018
# Connection string to your Azure AI Foundry project
2119
# Currently, it should be in the format "<HostName>;<AzureSubscriptionId>;<ResourceGroup>;<HubName>"
2220
PROJECT_CONNECTION_STRING = "eastus2.api.azureml.ms;80d2c6c6-fa64-4ab1-8aa5-4e118c6b16ce;rg-aprilk-azure-ai-basic-01a;ai-project-e6pnryr2q3qeg"
2321

2422
# Your Application Insights resource ID
2523

26-
APPLICATION_INSIGHTS_RESOURCE_ID = "InstrumentationKey=224f19e4-ec6f-4b9b-8ddb-357ff3ca0394;IngestionEndpoint=https://eastus2-3.in.applicationinsights.azure.com/;LiveEndpoint=https://eastus2.livediagnostics.monitor.azure.com/;ApplicationId=f029aec6-4f79-4301-b405-53ae1469a3a5"
24+
APPLICATION_INSIGHTS_RESOURCE_ID = "/subscriptions/80d2c6c6-fa64-4ab1-8aa5-4e118c6b16ce/resourceGroups/rg-aprilk-azure-ai-basic-01a/providers/Microsoft.Insights/components/appi-e6pnryr2q3qeg"
2725

2826
# Kusto Query Language (KQL) query to query data from Application Insights resource
2927
# This query is compatible with data logged by the Azure AI Inferencing Tracing SDK (linked in documentation)
@@ -52,7 +50,7 @@
5250

5351
# This is your Azure OpenAI Service connection name, which can be found in your Azure AI Foundry project under the 'Models + Endpoints' tab.
5452
default_connection = project_client.connections._get_connection(
55-
"aoai-e6pnryr2q3qeg_aoai"
53+
"aoai-e6pnryr2q3qeg"
5654
)
5755

5856
model_config = {
@@ -66,11 +64,11 @@
6664
# id for each evaluator can be found in your Azure AI Foundry registry - please see documentation for more information
6765
# init_params is the configuration for the model to use to perform the evaluation
6866
# data_mapping is used to map the output columns of your query to the names required by the evaluator
69-
relevance_evaluator_config = EvaluatorConfiguration(
70-
id="azureml://registries/azureml-staging/models/Relevance-Evaluator/versions/4",
71-
init_params={"model_config": model_config},
72-
data_mapping={"query": "${data.Input}", "response": "${data.Output}"}
73-
)
67+
# relevance_evaluator_config = EvaluatorConfiguration(
68+
# id="azureml://registries/azureml-staging/models/Relevance-Evaluator/versions/4",
69+
# init_params={"model_config": model_config},
70+
# data_mapping={"query": "${data.Input}", "response": "${data.Output}"}
71+
# )
7472

7573
# CoherenceEvaluator
7674
coherence_evaluator_config = EvaluatorConfiguration(
@@ -84,15 +82,14 @@
8482

8583
# Dictionary of evaluators
8684
evaluators = {
87-
"relevance": relevance_evaluator_config,
8885
"coherence" : coherence_evaluator_config
8986
}
9087

9188
name = SAMPLE_NAME
9289
description = f"{SAMPLE_NAME} description"
9390
# AzureMSIClientId is the clientID of the User-assigned managed identity created during set-up - see documentation for how to find it
9491
# https://ms.portal.azure.com/#view/Microsoft_AAD_IAM/ManagedAppMenuBlade/~/Overview/objectId/83c77e9f-bbc1-41a6-8956-4e36e992336f/appId/4610a06d-56a0-47ab-aeb6-cf95bc662052
95-
properties = {"AzureMSIClientId": "c623a44d-a3b9-4485-95cc-db46967444e4", "Environment": "azureml://registries/azureml/environments/azureml-evaluations-built-in/versions/9"}
92+
properties = {"AzureMSIClientId": "c623a44d-a3b9-4485-95cc-db46967444e4", "Environment": "azureml://registries/azureml/environments/azureml-evaluations-built-in/versions/14"}
9693

9794
# Configure the online evaluation schedule
9895
evaluation_schedule = EvaluationSchedule(
@@ -103,5 +100,5 @@
103100
properties=properties)
104101

105102
# Create the online evaluation schedule
106-
#created_evaluation_schedule = project_client.evaluations.create_or_replace_schedule(name, evaluation_schedule)
107-
#print(f"Successfully submitted the online evaluation schedule creation request - {created_evaluation_schedule.name}, currently in {created_evaluation_schedule.provisioning_state} state.")
103+
created_evaluation_schedule = project_client.evaluations.create_or_replace_schedule(name, evaluation_schedule)
104+
print(f"Successfully submitted the online evaluation schedule creation request - {created_evaluation_schedule.name}, currently in {created_evaluation_schedule.provisioning_state} state.")

src/api/main.py

Lines changed: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66

77
import fastapi
88
from azure.ai.projects.aio import AIProjectClient
9+
from azure.ai.projects.models import ConnectionType
910
from azure.ai.inference.prompts import PromptTemplate
1011
from azure.identity import AzureDeveloperCliCredential, ManagedIdentityCredential
1112
from dotenv import load_dotenv
1213
from fastapi.staticfiles import StaticFiles
1314

1415
from .shared import globals
16+
from .routes import get_targeting_context
1517

1618
from azure.ai.inference.tracing import AIInferenceInstrumentor
1719
from azure.monitor.opentelemetry import configure_azure_monitor
@@ -21,13 +23,19 @@
2123
from featuremanagement import FeatureManager
2224
from featuremanagement.azuremonitor import publish_telemetry
2325

26+
from featuremanagement import FeatureManager
27+
from featuremanagement.azuremonitor import TargetingSpanProcessor,publish_telemetry
28+
29+
2430
from opentelemetry.baggage import get_baggage
2531
from opentelemetry.sdk.trace import Span, SpanProcessor
2632
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
2733
from opentelemetry.baggage import set_baggage
2834
from opentelemetry.context import attach
2935
from opentelemetry.sdk.trace import Span
3036

37+
from azure.core.settings import settings
38+
3139
import uuid
3240

3341
logger = logging.getLogger("azureaiapp")
@@ -53,13 +61,27 @@ async def lifespan(app: fastapi.FastAPI):
5361
credential=azure_credential,
5462
conn_str=os.environ["AZURE_AIPROJECT_CONNECTION_STRING"],
5563
)
64+
# default_connection = await project.connections.get_default(connection_type=ConnectionType.AZURE_OPEN_AI)
65+
# deployment_name = os.environ["AZURE_AI_CHAT_DEPLOYMENT_NAME"]
66+
# api_version = "2024-08-01-preview"
67+
# model_config = default_connection.to_evaluator_model_config(
68+
# deployment_name=deployment_name, api_version=api_version
69+
# )
70+
model_config = {
71+
"type": "azure_openai",
72+
"azure_deployment": "gpt-4o-mini",
73+
"api_version": "2024-08-01-preview",
74+
"azure_endpoint": "https://aoai-e6pnryr2q3qeg.openai.azure.com/"
75+
}
5676

5777
chat = await project.inference.get_chat_completions_client()
5878
prompt = PromptTemplate.from_prompty(pathlib.Path(__file__).parent.resolve() / "prompt.v1.prompty")
5979

80+
6081
# Enable tracing
6182
application_insights_connection_string = await project.telemetry.get_connection_string()
62-
configure_azure_monitor(connection_string=application_insights_connection_string, span_processors=[TargetingSpanProcessor()])
83+
configure_azure_monitor(connection_string=application_insights_connection_string, span_processors=[TargetingSpanProcessor(targeting_context_accessor=get_targeting_context)])
84+
settings.tracing_implementation = "opentelemetry"
6385
AIInferenceInstrumentor().instrument()
6486

6587
# Inititalize the feature manager
@@ -71,36 +93,41 @@ async def lifespan(app: fastapi.FastAPI):
7193
feature_flag_refresh_enabled=True,
7294
refresh_interval=30, # 30 seconds
7395
)
74-
feature_manager = FeatureManager(app_config, on_feature_evaluated=publish_telemetry)
96+
feature_manager = FeatureManager(app_config, targeting_context_accessor=get_targeting_context, on_feature_evaluated=publish_telemetry)
97+
#feature_manager = FeatureManager(app_config, on_feature_evaluated=publish_telemetry)
98+
99+
75100

101+
76102
globals["project"] = project
77103
globals["chat"] = chat
78104
globals["prompt"] = prompt
79105
globals["chat_model"] = os.environ["AZURE_AI_CHAT_DEPLOYMENT_NAME"]
80106
globals["feature_manager"] = feature_manager
107+
globals["model_config"] = model_config
81108

82109
yield
83110

84111
await project.close()
85112

86113
await chat.close()
87114

88-
# Below will be replaced by a helper function from App Config SD
115+
# Below will be replaced by a helper function from App Config SDK
89116

90-
class TargetingSpanProcessor(SpanProcessor):
91-
def on_start(
92-
self,
93-
span: "Span",
94-
parent_context = None,
95-
):
96-
if (get_baggage("Microsoft.TargetingId", parent_context) != None):
97-
span.set_attribute("TargetingId", get_baggage("Microsoft.TargetingId", parent_context))
117+
# class TargetingSpanProcessor(SpanProcessor):
118+
# def on_start(
119+
# self,
120+
# span: "Span",
121+
# parent_context = None,
122+
# ):
123+
# if (get_baggage("Microsoft.TargetingId", parent_context) != None):
124+
# span.set_attribute("TargetingId", get_baggage("Microsoft.TargetingId", parent_context))
98125

99-
def server_request_hook(span: Span, scope: dict[str, Any]):
100-
if span and span.is_recording():
101-
targeting_id = str(uuid.uuid4())
102-
attach(set_baggage("Microsoft.TargetingId", targeting_id))
103-
span.set_attribute("TargetingId", targeting_id)
126+
# def server_request_hook(span: Span, scope: dict[str, Any]):
127+
# if span and span.is_recording():
128+
# targeting_id = str(uuid.uuid4())
129+
# attach(set_baggage("Microsoft.TargetingId", targeting_id))
130+
# span.set_attribute("TargetingId", targeting_id)
104131

105132
# End Targeting Id code
106133

@@ -118,6 +145,6 @@ def create_app():
118145

119146
app.include_router(routes.router)
120147

121-
FastAPIInstrumentor.instrument_app(app, server_request_hook=server_request_hook)
148+
FastAPIInstrumentor.instrument_app(app) #, server_request_hook=server_request_hook)
122149

123150
return app

src/api/routes.py

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@
1313
from azure.ai.inference.prompts import PromptTemplate
1414

1515
from .shared import globals
16-
from azure.core.settings import settings
1716

1817
from opentelemetry.baggage import get_baggage
19-
from azure.ai.inference.aio import ChatCompletionsClient
18+
from azure.ai.evaluation import CoherenceEvaluator, FluencyEvaluator, RelevanceEvaluator, ViolenceEvaluator, SexualEvaluator, HateUnfairnessEvaluator, ProtectedMaterialEvaluator, ContentSafetyEvaluator
19+
import asyncio
20+
from opentelemetry.baggage import set_baggage, get_baggage
21+
from opentelemetry.context import attach
22+
from featuremanagement import TargetingContext
2023

21-
settings.tracing_implementation = "opentelemetry"
2224
router = fastapi.APIRouter()
2325
templates = Jinja2Templates(directory="api/templates")
2426

@@ -31,6 +33,7 @@ class Message(pydantic.BaseModel):
3133
class ChatRequest(pydantic.BaseModel):
3234
messages: list[Message]
3335
prompt_override: str = None
36+
sessionState: dict = {}
3437

3538
@router.get("/test/hello")
3639
async def test():
@@ -58,7 +61,7 @@ async def response_stream():
5861
if chat_request.prompt_override:
5962
prompt = PromptTemplate.from_prompty(pathlib.Path(__file__).parent.resolve() / chat_request.prompt_override)
6063
else:
61-
prompt_variant = feature_manager.get_variant("prompty_file", targeting_id) # replace this with prompt_asset
64+
prompt_variant = feature_manager.get_variant("prompty_file") # replace this with prompt_asset
6265
if prompt_variant and prompt_variant.configuration:
6366
prompt = PromptTemplate.from_prompty(pathlib.Path(__file__).parent.resolve() / prompt_variant.configuration)
6467
else:
@@ -88,9 +91,13 @@ async def response_stream():
8891
return fastapi.responses.StreamingResponse(response_stream())
8992

9093

94+
def get_targeting_context():
95+
return TargetingContext(user_id=get_baggage("Microsoft.TargetingId"))
96+
9197
@router.post("/chat")
9298
async def chat_nostream_handler(
93-
chat_request: ChatRequest
99+
chat_request: ChatRequest,
100+
request: Request
94101
):
95102
chat_client = globals["chat"]
96103
if chat_client is None:
@@ -99,15 +106,20 @@ async def chat_nostream_handler(
99106
messages = [{"role": message.role, "content": message.content} for message in chat_request.messages]
100107
model_deployment_name = globals["chat_model"]
101108
feature_manager = globals["feature_manager"]
102-
targeting_id = get_baggage("Microsoft.TargetingId") or str(uuid.uuid4())
109+
110+
targeting_id = chat_request.sessionState['sessionId'] or str(uuid.uuid4())
111+
attach(set_baggage("Microsoft.TargetingId", targeting_id))
103112

104113
# figure out which prompty template to use (replace file to API)
114+
variant = "none"
105115
if chat_request.prompt_override:
106116
prompt = PromptTemplate.from_prompty(pathlib.Path(__file__).parent.resolve() / chat_request.prompt_override)
117+
variant = chat_request.prompt_override
107118
else:
108-
prompt_variant = feature_manager.get_variant("prompty_file", targeting_id) # replace this with prompt_asset
119+
prompt_variant = feature_manager.get_variant("prompty_file") # replace this with prompt_asset
109120
if prompt_variant and prompt_variant.configuration:
110121
prompt = PromptTemplate.from_prompty(pathlib.Path(__file__).parent.resolve() / prompt_variant.configuration)
122+
variant = prompt_variant.name
111123
else:
112124
prompt = globals["prompt"]
113125

@@ -117,9 +129,40 @@ async def chat_nostream_handler(
117129
response = await chat_client.complete(
118130
model=model_deployment_name, messages=prompt_messages + messages, stream=False
119131
)
132+
track_event("RequestMade", targeting_id)
120133
except Exception as e:
121134
error = {"Error": str(e)}
122135
track_event("ErrorLLM", targeting_id, error)
123136

124137
answer = response.choices[0].message.content
125-
return answer
138+
139+
# eval_sampling = feature_manager.get_variant("eval_sampling", targeting_id)
140+
# if eval_sampling and eval_sampling.configuration == True:
141+
# eval_input = { "conversation": { "messages": messages } }
142+
# project = globals["project"]
143+
#asyncio.create_task(run_evals(eval_input, targeting_id, project.scope, DefaultAzureCredential()))
144+
145+
return { "answer": answer, "variant": variant }
146+
147+
async def run_evals(eval_input, targeting_id, ai_project_scope, credential):
148+
run_eval(FluencyEvaluator, eval_input, targeting_id)
149+
run_eval(RelevanceEvaluator, eval_input, targeting_id)
150+
run_eval(CoherenceEvaluator, eval_input, targeting_id)
151+
152+
run_safety_eval(ViolenceEvaluator, eval_input, targeting_id, ai_project_scope, credential)
153+
run_safety_eval(SexualEvaluator, eval_input, targeting_id, ai_project_scope, credential)
154+
run_safety_eval(HateUnfairnessEvaluator, eval_input, targeting_id, ai_project_scope, credential)
155+
run_safety_eval(ProtectedMaterialEvaluator, eval_input, targeting_id, ai_project_scope, credential)
156+
run_safety_eval(ContentSafetyEvaluator, eval_input, targeting_id, ai_project_scope, credential)
157+
158+
def run_safety_eval(evaluator, eval_input, targeting_id, ai_project_scope, credential):
159+
eval = evaluator(credential=credential, azure_ai_project=ai_project_scope)
160+
score = eval(**eval_input)
161+
score.update({"evaluator_id": eval.id})
162+
track_event("gen.ai." + type(eval).__name__, targeting_id, score)
163+
164+
def run_eval(evaluator, eval_input, targeting_id):
165+
eval = evaluator(globals["model_config"])
166+
score = eval(**eval_input)
167+
score.update({"evaluator_id": evaluator.id})
168+
track_event("gen.ai." + evaluator.__name__, targeting_id, score)

0 commit comments

Comments
 (0)