How to integrate your live SAP HR Data into M365 Copilot - Part 2

AI Jun 1, 2024
Read the previous post before you read this. At the end of this series, you will get the complete github project to play around with your data.

In my last article, I wrote about how the m365 Copilot works and how we created a new Plugin / Bot that will later used by M365 Copilot. In this article, I will try to describe how we will extract the required Information from the description and fetch the required data from the SAP Systemto generate a matching result.

How to extract the required data

Let's assume you create a model deployment within your Azure open AI instance. (I use the gpt 3.5 turbo, this will be enough for our needs).

So basically the technical flow looks like this:

The end user types in the description from the tender, and the bot will take it and send it to a GPT 3.5 model to extract the required information (like skills, job title, duration, and so on). The funny thing is, that I can tell the model to act as an API and it will send me the JSON result back.

Next, I go to SAP and request the Employees that match the required skills (and hourly rates, as well as the availability).

The result will be sent to the GPT again to form a textual result. This last step is finally done automatically by the m365 copilot itself.

Extend the code to fulfill the extraction

So at the moment, we use s.th. like "Hey search me c# developers" or s.th. But I want to insert a tender description and let the Azure AI extract the required skills and ask SAP against these skills.

So, I created a small helper method within the "MyDataSource" class.


    public async GetResult(message: string): Promise<string> {

        const messages = [
            { role: "system", content: `Du bist eine schnittstelle und gibst die Antworten in einem JSON zurück. Extrahiere aus den Eingaben in den '<context></context>' nur die technischen skills, zusätzlich die Laufzeiten und rechne diese in Monaten um. Gebe die Remotezeit in Prozent and. Des Weiteren der Einsatzort,  und der Jobtitel. Beispiel JSON als Ausgabe referenz sieht wie folgt aus 
{
"RequiredSkills":[
    {
        "Name":".Net",
    },
    {
        "Name":"SAP",
    }
],
"OptionalSkills":[
    {
        "Name":"Documentation",
    },
    {
        "Name":"Music",
    }
]

"JobTitle": "C# .Net Developer",
"Duration": 2,
"PercentRemote": 15
} 
wenn die Eigenschaften nicht ausgelesen werden können, sind diese nicht einzutragen. Wenn Überhaupt keine Daten ermittelt werden können, dann ist folgendes JSON zurück zu liefern
{
    "JobTitle": "N/A",
}
Gebe NUR das JSON aus denn du bist eine API-Schnittstelle. 
` },
            { role: "user", content: message },
        ];

        let result: string = "";
        const events = await this.client.streamChatCompletions(deploymentId, messages, { maxTokens: 800 },);
        for await (const event of events) {
            for (const choice of event.choices) {
                if (choice.delta?.content != undefined) {
                    result += choice.delta?.content;
                }
            }
        }
        return result;
    }

Let me explain this. I will configure the Open AI service and tell him to act as an API that only results in JSONs. But not any JSON, I will give him the structure with an example. So, it can now parse the input and extract the data into the required result.

Let's take this example (it's German sorry, but it fits for now, AI can speak all languages ;))

FĂĽr unseren Kunden suchen wir einen .Net / Blazor Spezialisten (m/w/d).  
  
Start: Juni/ späterer Start, wenn von dir gewünscht möglich (Juli)  
Laufzeit: 6 Monate ++  
Einsatzort: Remote  
Auslastung: 40%  
  
Aufgaben und Anforderungen:  
- Technische Beratung und Umsetzng im Bereich der Webentwicklung mit dem Ziel, die Leistungsfähigkeit der aktuellen IT zu verbessern:  
? Entwicklung von Webanwendungen mit ASP.NET Blazor  
? Entwicklung von Benutzeroberflächen  
? Entwicklung mit .NET 7 und Steigerung der Performance mit den neuesten Features des Frameworks  
? Bereitstellung und Verwaltung von containerisierten Anwendungen in einer Kubernetes-Umgebung  
? Einrichten von CI/CD-Pipelines und Optimieren mit Azure DevOps  
? Idealerweise: SAP PI / ODATA Erfahrung - nicht selbst implenetiert, sonder mit einer SAP API schonmal kommuniziert (nice-to-have)  
? FlieĂźend in Deutsch und Englisch  
  
Hast Du freie Kapazitäten und kannst unseren Kunden unterstützen?  
Ich freue mich auf Deine RĂĽckmeldung mit der Projektnummer folgenden Informationen an :  
• aktuelle Projektverfügbarkeit (frühester Start)  
• maximale Auslastung/Woche insgesamt  
• Stundensatz  
• aktuelles Profil (idealerweise im pdf Format)  
• Einschätzung zu den geforderten Anforderungen  
  
  
Wir bearbeiten alle Rückmeldungen und geben immer unser Bestes, uns bei allen Kandidaten (m/w/d) zurückzumelden. Leider ist dies nicht immer möglich, wir bitten um Dein Verständnis. Wenn wir uns innerhalb von 5 Werktagen nicht bei Dir melden, gehe bitte davon aus, dass der Kunde sich für einen anderen Kandidaten entschieden hat.  
  
Beste GrĂĽĂźe  

So when Open AI gets this description, it will extract the information. Here is an example of what it looks like:

0:00
/0:05

Now we get structured data, that we can use technically.

Selecting data from SAP with the extracted requirement skills

Previously we saw that we extracted the required information from the tender description. So next we must fetch the relevant data from the SAP System.

Let's take the example that I get a search for the skill "Java" In this case the query to the API looks like this

https://api.successfactors.com/odata/v2/User?$filter=skills/any(s:s/name eq 'Java')

This will result in the following answer (PII are replaced)

{
  "d": {
    "results": [
      {
        "__metadata": {
          "uri": "https://api.successfactors.com/odata/v2/User('user1')",
          "type": "SFOData.User"
        },
        "userId": "user1",
        "firstName": "John",
        "lastName": "Doe",
        "skills": [
          {
            "__metadata": {
              "uri": "https://api.successfactors.com/odata/v2/Skill('skill1')",
              "type": "SFOData.Skill"
            },
            "name": "Java",
            "proficiency": "Expert",
            "yearsOfExperience": 5
          }
        ]
      },
      {
        "__metadata": {
          "uri": "https://api.successfactors.com/odata/v2/User('user2')",
          "type": "SFOData.User"
        },
        "userId": "user2",
        "firstName": "Jane",
        "lastName": "Smith",
        "skills": [
          {
            "__metadata": {
              "uri": "https://api.successfactors.com/odata/v2/Skill('skill1')",
              "type": "SFOData.Skill"
            },
            "name": "Java",
            "proficiency": "Intermediate",
            "yearsOfExperience": 3
          }
        ]
      }
    ]
  }
}

So in this case I got two Employees that match these skills. It will also return the proficiency and the year of their experience. So my final code looks like this:

import { PersonalInformation, Skill } from "./Interfaces";
import axios from 'axios';


/**
 * Skill Response from SAP
 */
interface SapResponseSkill {
    name: string;
    proficiency: string;
    yearsOfExperience: number;
}

/**
 * User response from SAP
 */
interface SapResponseUser {
    userId: string;
    firstName: string;
    lastName: string;
    skills: SapResponseSkill[];
}
/**
 * Result response from SAP itself.
 */
interface SapResponseApiResponse {
    d: {
        results: SapResponseUser[];
    };
}

export default class SapHcmService {
    private token:string; 
    
    constructor() {

        this.token= "your_oauth_token_here";

    }

    /**
     * Get the data from SAP.
     * 
     * @param requiredSkills 
     * @returns 
     */
    public async Fetch(requiredSkills: Skill[]): Promise<PersonalInformation[]> {
        
        let result: PersonalInformation[] = [];

        const users = await this.getUsersBySkills(requiredSkills, this.token);

        users.forEach((_: SapResponseUser) => {
            let user: PersonalInformation = {
                Firstname: _.firstName,
                LastName: _.lastName,
                FullName: "".concat(_.lastName, ", ", _.firstName),
                Availbility: 100,
                Cv: '',                 // Future implementation
                EMail: "".concat(_.firstName, ".", _.lastName,"@domain.com"),
                ImageLocation: '' ,     // Future impplementation
                LastUpdate: new Date(),
                SkillsMatches: []
            };
            _.skills.forEach(_ => {
                user.SkillsMatches.push({
                    name: _.name,
                    proficiency: _.proficiency,
                    yearsOfExperience: _.yearsOfExperience
                })
            });

            result.push(user);
        });
        return result;
    }

    /**
     * Get the user by their skills from the SAP System
     * @param skills 
     * @param token 
     * @returns 
     */
    private async getUsersBySkills(skills: Skill[], token: string): Promise<SapResponseUser[]> {
        const url = 'https://api.successfactors.com/odata/v2/User';
        const filterQuery = skills.map(skill => `skills/any(s:s/name eq '${skill.Name}')`).join(' and ');
        const requestUrl = `${url}?$filter=${filterQuery}`;

        try {
            const response = await axios.get<SapResponseApiResponse>(requestUrl, {
                headers: {
                    Authorization: `Bearer ${token}`,
                    'Content-Type': 'application/json'
                }
            });

            if (response.status === 200) {
                return response.data.d.results;
            } else {
                throw new Error(`Error: ${response.status} - ${response.statusText}`);
            }
        } catch (error) {
            console.error('Error fetching users by skills:', error);
            throw error;
        }
    }
}

The main call will be the fetch method. This will get all skills to select for the current tender. So after that, I will build the query against the SAP System itself and fetch the desired users.

The result will then be transformed into the result object called PersonalInformation. This will later be used to generate the Herocard output.

Generate an answer for the user

So in fact we use actually the bot plugin from Teams. So in this case, we generate an answer combined with the resulting data from the SAP System. So That is a small "grounding".

So in this case, you open up the bot itself, ask "Hey inspect this tender ..... and generate the matching employees as table output" It will then do the following procedure (like above)

  1. Extract the required skills
  2. Fetching the matches from SAP

Now it must be sent back to the user itself, but instead of sending him the plain objects, we must send them through the LLM itself. So that he gets a smooth nice output to the user.

So we generated the bot plugin earlier that will use an openai llm from Azure. This requires a system message, to tell how the LLM must act. In my case, It use this (sorry for the german wording):

Du bist ein Hilfsbereiter Agent. 
wenn <context></context> angegeben ist dann mache folgendes:

In <context></context> sind die Eckdaten ĂĽber die Ausschreibung die einmal kurz zusammengefasst werden muss. In  <personas></personas> stehen die dazu gefundenen Mitarbeiter. 
Gebe die Mitarbeiter als Tabelle aus und errechne ein Match score. Denk dir keine Daten aus und verwende nur die vorliegenden.

Wenn kein <context></context> und <personas></personas> angegeben ist, dann beantworte die Fragen auf den vorherigen Kontext.

So in general, it will act as a helpful agent, it will use the data in <context> as the tender data, to summarize it to the user. Then in the <personas> tags, there will be the matched employees.

So finally it will summarize the tender and write out the list of the matched employees.

So basically there is a "handler" the will workout all of it. Here is the code for this:

import { MemoryStorage } from "botbuilder";
import * as path from "path";
import config from "../config";

// See https://aka.ms/teams-ai-library to learn more about the Teams AI library.
import { Application, ActionPlanner, OpenAIModel, PromptManager, TurnState } from "@microsoft/teams-ai";
import { MyDataSource } from "./myDataSource";

// Create AI components
const model = new OpenAIModel({
  azureApiKey: config.azureOpenAIKey,
  azureDefaultDeployment: config.azureOpenAIDeploymentName,
  azureEndpoint: config.azureOpenAIEndpoint,

  useSystemMessages: true,
  logRequests: true,
});

// Get the promts
const prompts = new PromptManager({
  promptsFolder: path.join(__dirname, "../prompts"),
});

// Initialize the planner for the chat itselff
const planner = new ActionPlanner<TurnState>({
  model,
  prompts,
  defaultPrompt: "chat",
});

// Register your data source with planner
const myDataSource = new MyDataSource("my-ai-search");
myDataSource.init();
planner.prompts.addDataSource(myDataSource);

// Define storage and application
const storage = new MemoryStorage();

// Will handle all Chat requests
const app = new Application<TurnState>({
  storage,
  ai: {
    planner,
  },
});

export default app;

So at first, I create the openAi Model definition and tell the app to use the model from my Azure instance. Next, I will load the prompt (described above). After that, I will define a planner that will "orchestrate" the combination of the openai model, the prompt, and the data source itself.

So In this case the datasource will be the logic for extracting the skills from the tender (described above).

So this "app" will then used at the message endpoint and will handle from now on the chat requests

server.post("/api/messages", async (req, res) => {
  // Route received a request to adapter for processing
  await adapter.process(req, res as any, async (context) => {
    // Dispatch to application for routing
    await app.run(context);
  });
});

So for now you will have at the first step a new bot created that will use the natural language to generate a "natural" output for employee predictions.

But you might scream "Hey what about copilot? You promised that we integrate this into copilot?" .

This will be part of my next post, because I must learn basics first.

Tags