Skip to content

Commit

Permalink
Feature/ocs/ai logging (#1)
Browse files Browse the repository at this point in the history
* Add Application Insights Provider

* Change from CRLF to LF

* Change biceps line ending

* Refactor ApplicationInsightsProvider and initializeTelemetry

* Add session parameter to initializeTelemetry function

* Add logger and track metrics for prompt and completion tokens

* Add OpenTelemetry instrumentation for metrics tracking

* Update model encoding to "gpt-4"

* Add OpenTelemetry metrics instrumentation

* Remove cleanup step

* Add chat metrics tracking and token service
  • Loading branch information
saoc90 authored Feb 6, 2024
1 parent d213f46 commit 9569f9b
Show file tree
Hide file tree
Showing 13 changed files with 2,637 additions and 28 deletions.
7 changes: 1 addition & 6 deletions .github/workflows/open-ai-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,10 @@ jobs:
resource-group-name: ${{ secrets.AZURE_APP_SERVICE_RG_NAME_DEV }}
app-name: ${{ secrets.AZURE_APP_SERVICE_NAME_DEV }}
package: ${{ github.workspace }}/Nextjs-site.zip

- name: 🧹 Cleanup
run: rm ${{ github.workspace }}/Nextjs-site.zip

deploy-production:
runs-on: ubuntu-latest
needs: deploy-development
needs: build
environment:
name: "Production"
url: ${{ steps.deploy-to-webapp.outputs.webapp-url }} # or your production URL, add reviewers as well if you need
Expand Down Expand Up @@ -131,6 +128,4 @@ jobs:
app-name: ${{ secrets.AZURE_APP_SERVICE_NAME_PROD }}
package: ${{ github.workspace }}/Nextjs-site.zip

- name: 🧹 Cleanup
run: rm ${{ github.workspace }}/Nextjs-site.zip

17 changes: 17 additions & 0 deletions infra/resources.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ var cosmos_name = toLower('${name}-cosmos-${resourceToken}')
var search_name = toLower('${name}search${resourceToken}')
var webapp_name = toLower('${name}-webapp-${resourceToken}')
var appservice_name = toLower('${name}-app-${resourceToken}')
var appInsights_name = toLower('${name}-ai-${resourceToken}')
// keyvault name must be less than 24 chars - token is 13
var kv_prefix = take(name, 7)
var keyVaultName = toLower('balm-chat-${resourceToken}')
Expand Down Expand Up @@ -171,6 +172,10 @@ resource webApp 'Microsoft.Web/sites@2020-06-01' = {
name: 'AZURE_SPEECH_KEY'
value: '@Microsoft.KeyVault(VaultName=${kv.name};SecretName=${kv::AZURE_SPEECH_KEY.name})'
}
{
name: 'NEXT_PUBLIC_APPINSIGHTS_INSTRUMENTATIONKEY'
value: appInsights.properties.InstrumentationKey
}
]
}
}
Expand All @@ -182,6 +187,18 @@ resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2021-12
location: location
}

resource appInsights 'Microsoft.Insights/components@2020-02-02' = {
name: appInsights_name
kind: 'web'
location: location
tags: tags
properties: {
WorkspaceResourceId: logAnalyticsWorkspace.id
Application_Type: 'web'
Request_Source: 'rest'
}
}

resource webDiagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = {
name: diagnostic_setting_name
scope: webApp
Expand Down
20 changes: 20 additions & 0 deletions src/app/application-insights-provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
'use client'

import { AppInsightsContext } from '@microsoft/applicationinsights-react-js'
import { createContext } from 'react'
import { initializeTelemetry } from './application-insights-service'
import { useSession } from 'next-auth/react'

export const ApplicationInsightsContext = createContext({})

export default function ApplicationInsightsProvider({
instrumentationKey,
children,
}: {
instrumentationKey: string,
children: React.ReactNode
}) {
const session = useSession()
const { reactPlugin } = initializeTelemetry(instrumentationKey, session)
return <AppInsightsContext.Provider value={reactPlugin}>{children}</AppInsightsContext.Provider>
}
55 changes: 55 additions & 0 deletions src/app/application-insights-service.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"use client"
import {ApplicationInsights, ITelemetryItem} from '@microsoft/applicationinsights-web';
import {ReactPlugin} from '@microsoft/applicationinsights-react-js';
import { SessionContextValue } from 'next-auth/react';

let logger: ApplicationInsights;

function initializeTelemetry(instrumentationKey: string, session: SessionContextValue): { reactPlugin: ReactPlugin, appInsights: ApplicationInsights } {

const defaultBrowserHistory = {
url: "/",
location: { pathname: ""},
state: { url: "" },
listen: () => {},
};

let browserHistory = defaultBrowserHistory;

if (typeof window !== "undefined") {
browserHistory = { ...browserHistory, ...window.history };
browserHistory.location.pathname = browserHistory?.state?.url;
}

const reactPlugin = new ReactPlugin();
const appInsights = new ApplicationInsights({
config: {
instrumentationKey: instrumentationKey,
extensions: [reactPlugin],
extensionConfig: {
[reactPlugin.identifier]: { history: browserHistory },
},
enableAutoRouteTracking: true,
disableAjaxTracking: false,
autoTrackPageVisitTime: true,
enableCorsCorrelation: true,
enableRequestHeaderTracking: true,
enableResponseHeaderTracking: true,
}
});

appInsights.loadAppInsights();

appInsights.addTelemetryInitializer((env:ITelemetryItem) => {
env.tags = env.tags || [];
env.tags["ai.cloud.role"] = "Bühler ChatGPT";
env.data = env.data || [];
env.data["email"] = session?.data?.user?.email;
});

logger = appInsights;

return { reactPlugin, appInsights };
}

export { initializeTelemetry, logger };
41 changes: 23 additions & 18 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { AI_NAME } from "@/features/theme/customise";
import { cn } from "@/lib/utils";
import { Inter } from "next/font/google";
import "./globals.css";
import ApplicationInsightsProvider from "./application-insights-provider";
import { unstable_noStore as noStore } from 'next/cache'

export const dynamic = "force-dynamic";

Expand All @@ -21,27 +23,30 @@ export default function RootLayout({
}: {
children: React.ReactNode;
}) {
noStore()
const instrumentationKey = process.env.APPINSIGHTS_INSTRUMENTATIONKEY || "";
return (
<html lang="en" className="h-full overflow-hidden">
<body className={cn(inter.className, "flex w-full h-full")}>
<GlobalConfigProvider
config={{ speechEnabled: process.env.PUBLIC_SPEECH_ENABLED }}
>
<Providers>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<div
className={cn(
inter.className,
"flex w-full p-2 h-full gap-2 bg-primary"
)}
>
{children}
</div>

<Toaster />
</ThemeProvider>
</Providers>
</GlobalConfigProvider>
<GlobalConfigProvider
config={{ speechEnabled: process.env.PUBLIC_SPEECH_ENABLED }}
>
<Providers>
<ApplicationInsightsProvider instrumentationKey={instrumentationKey}>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<div
className={cn(
inter.className,
"flex w-full p-2 h-full gap-2 bg-primary"
)}
>
{children}
</div>
<Toaster />
</ThemeProvider>
</ApplicationInsightsProvider>
</Providers>
</GlobalConfigProvider>
</body>
</html>
);
Expand Down
23 changes: 23 additions & 0 deletions src/features/chat/chat-services/chat-api-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { similaritySearchVectorWithScore } from "./azure-cog-search/azure-cog-ve
import { initAndGuardChatSession } from "./chat-thread-service";
import { CosmosDBChatMessageHistory } from "./cosmosdb/cosmosdb";
import { PromptGPTProps } from "./models";
import { ChatTokenService } from "./chat-token-service";
import { reportCompletionTokens, reportPromptTokens, reportUserChatMessage } from "./chat-metrics-service";

const SYSTEM_PROMPT = `You are ${AI_NAME} who is a helpful AI Assistant.`;

Expand Down Expand Up @@ -35,6 +37,8 @@ export const ChatAPIData = async (props: PromptGPTProps) => {

const openAI = OpenAIInstance();

const chatModel = "gpt-4";

const userId = await userHashedId();

const chatHistory = new CosmosDBChatMessageHistory({
Expand All @@ -45,6 +49,8 @@ export const ChatAPIData = async (props: PromptGPTProps) => {
const history = await chatHistory.getMessages();
const topHistory = history.slice(history.length - 30, history.length);

const tokenService = new ChatTokenService();

const relevantDocuments = await findRelevantDocuments(
lastHumanMessage.content,
id
Expand All @@ -58,7 +64,16 @@ export const ChatAPIData = async (props: PromptGPTProps) => {
})
.join("\n------\n");

const contextTokens = tokenService.getTokenCount(context);

let promptTokens = contextTokens + 122; // 122 is static system prompt tokens.

promptTokens += tokenService.getTokenCountFromHistory(topHistory, 0);

try {

reportPromptTokens(promptTokens, chatModel);

const response = await openAI.chat.completions.create({
messages: [
{
Expand All @@ -78,6 +93,8 @@ export const ChatAPIData = async (props: PromptGPTProps) => {
stream: true,
});

let completionTokens = 0;

const stream = OpenAIStream(response, {
async onCompletion(completion) {
await chatHistory.addMessage({
Expand All @@ -92,7 +109,13 @@ export const ChatAPIData = async (props: PromptGPTProps) => {
},
context
);

reportCompletionTokens(completionTokens, chatModel);
reportUserChatMessage(chatModel);
},
onToken(token) {
completionTokens += tokenService.getTokenCount(token);
}
});

return new StreamingTextResponse(stream);
Expand Down
22 changes: 21 additions & 1 deletion src/features/chat/chat-services/chat-api-simple.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { userHashedId } from "@/features/auth/helpers";
import { userHashedId, userSession } from "@/features/auth/helpers";
import { OpenAIInstance } from "@/features/common/openai";
import { AI_NAME } from "@/features/theme/customise";
import { OpenAIStream, StreamingTextResponse } from "ai";
import { initAndGuardChatSession } from "./chat-thread-service";
import { CosmosDBChatMessageHistory } from "./cosmosdb/cosmosdb";
import { PromptGPTProps } from "./models";
import { encodingForModel, TiktokenModel} from "js-tiktoken"
import { reportCompletionTokens, reportPromptTokens, reportUserChatMessage } from "./chat-metrics-service";
import { ChatTokenService } from "./chat-token-service";

export const ChatAPISimple = async (props: PromptGPTProps) => {
const { lastHumanMessage, chatThread } = await initAndGuardChatSession(props);
Expand All @@ -26,7 +29,15 @@ export const ChatAPISimple = async (props: PromptGPTProps) => {
const history = await chatHistory.getMessages();
const topHistory = history.slice(history.length - 30, history.length);

const tokenService = new ChatTokenService();

try {
const promptTokens = tokenService.getTokenCountFromHistory(topHistory, 45);

const model = "gpt-4";

reportPromptTokens(promptTokens, model);

const response = await openAI.chat.completions.create({
messages: [
{
Expand All @@ -41,14 +52,23 @@ export const ChatAPISimple = async (props: PromptGPTProps) => {
stream: true,
});

let completionTokens = 0;

const stream = OpenAIStream(response, {
async onToken(token) {
completionTokens += tokenService.getTokenCount(token);
},
async onCompletion(completion) {
await chatHistory.addMessage({
content: completion,
role: "assistant",
});

reportUserChatMessage(model);
reportCompletionTokens(completionTokens, model);
},
});

return new StreamingTextResponse(stream);
} catch (e: unknown) {
if (e instanceof Error) {
Expand Down
50 changes: 50 additions & 0 deletions src/features/chat/chat-services/chat-metrics-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { metrics } from "@opentelemetry/api";
import { userHashedId, userSession } from "@/features/auth/helpers";

function getChatMeter(){
const meter = metrics.getMeter("chat");
return meter;
}

async function getAttributes(chatModel: string){
const user = await userSession();
const userId = await userHashedId();
const attributes = { "email": user?.email, "name": user?.name, "userHashedId": userId, "chatModel": chatModel || "unknown", "userId": userId };
return attributes;
}

export async function reportPromptTokens(tokenCount: number, model: string) {

const meter = getChatMeter();

const promptTokensUsed = meter.createHistogram("promptTokensUsed", {
description: "Number of tokens used in the input prompt",
unit: "tokens",
});

promptTokensUsed.record(tokenCount, await getAttributes(model));
}

export async function reportCompletionTokens(tokenCount: number, model: string) {

const meter = getChatMeter();

const completionsTokensUsed = meter.createHistogram("completionsTokensUsed", {
description: "Number of tokens used in the completions",
unit: "tokens",
});

completionsTokensUsed.record(tokenCount, await getAttributes(model));
}

export async function reportUserChatMessage(model: string) {

const meter = getChatMeter();

const userChatMessage = meter.createCounter("userChatMessage", {
description: "Number of messages",
unit: "messages",
});

userChatMessage.add(1, await getAttributes(model));
}
Loading

0 comments on commit 9569f9b

Please sign in to comment.