{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# LLM for alle - Introduksjonskurs til språkmodeller med Python og Azure OpenAI"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Introduksjon\n",
    "\n",
    "Under et prosjekt vi gjorde da vi satt på benk i høst fikk vi hands-on erfaring med å utvikle og bruke språkmodeller (LLMer) i praksis. Vi så hvordan slike modeller kan hjelpe med å spare tid, redusere kostnader og automatisere manuelle oppgaver på en måte som enkelt kan gjenbrukes i andre prosjekter.\n",
    "\n",
    "I dette kurset ser vi på et konkret og gjenkjennbart case: analyse av resultatene fra en medarbeiderundersøkelse. Dette er en oppgave som kan være vanskelig å strukturere og som kan være ekstremt tidkrevende å gjøre for hånd når det er store volum av tilbakemeldinger. Vi skal lære dere å koble dere til en LLM via API, bruke LangChain Expression Language (LCEL) til å bygge AI-kjeder og hente ut strukturert innsikt med Pydantic, alt i én oversiktlig notebook.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Oppgave\n",
    "Du jobber i et IT-selskap og har fått i oppgave å analysere svarene fra en intern medarbeiderundersøkelse. Undersøkelsen er anonym og du har fått tilsendt en CSV-fil med 50 tilbakemeldinger, én per ansatt. Målet er å finne ut hva folk er fornøyde eller misfornøyde med, og særlig se nærmere på temaene Nettverk, Opplæring og IT-support, som ledelsen har gitt beskjed om at har vært sentrale temaer i undersøkelser de har sendt ut tidligere. Tilbakemeldinger som ikke passer i disse kategoriene skal også få sin plass. Til slutt skal du lage en oppsummering som kan sendes til ledelsen.\n",
    "\n",
    "For å jobbe effektivt bruker du en språkmodell til å hjelpe deg med både kategorisering og oppsummering. \n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Datasett"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Vi har fått rådatasettet levert som en csv-fil hvor vi i utgangspunktet kun er interessert i de 2 kolonnene \"ID\" og \"Feedback\". \"ID\" (int) er en unik id for hver ansatt og \"Feedback\" (str) inneholder tilbakemeldingene. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Import the raw data containing the feedback. There is one row per employee.\n",
    "\n",
    "import pandas as pd\n",
    "pd.set_option('display.max_colwidth', None) # Ensure no truncated output of dataframe\n",
    "\n",
    "path_to_survey = \"../files/IT_survey.csv\"\n",
    "df = pd.read_csv(\n",
    "    path_to_survey,\n",
    "    usecols = [\"ID\", \"Feedback\"],       # Extract relevant columns\n",
    "    dtype={\"ID\": int, \"Feedback\": str}  # Specify expected types\n",
    ")\n",
    "\n",
    "df.head()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Før vi går løs på oppgaven skal du bli bedre kjent med verktøyene og metodene vi skal bruke."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Koble til Azure OpenAI via LangChain"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### En vanlig LLM-spørring\n",
    "Enkle LLM-spørringer er bygget opp av noen sentrale deler:\n",
    "\n",
    "1. Tilkobling til en ressurs, som feks. Azure OpenAI\n",
    "\n",
    "2. En prompt, som vil si en tekstbasert forespørsel/instruks\n",
    "\n",
    "3. Sending av prompt til språkmodellen for å hente en respons\n",
    "\n",
    "\n",
    "Som en del av tilkoblingen er det vanlig å oppgi en temperaturparameter. Denne parametreren angir nivået av presisjon du ønsker å få i responsen fra språkmodellen, og kan enten måles på en skala fra \"low\" til \"high\" eller numerisk fra 0 til 1.\n",
    "Hvis denne parameteren settes til \"low\"/nærme 0 tillater du liten grad av variasjon og kreativitet i responsen, og du vil få tryggere og mer forutsigbare svar. Hvis den derimot settes til høy/nærme 1 tillater du større grad av kreativitet og detaljer, men vil følgelig også få en mer uforutsigbar respons. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Connect through the API\n",
    "from langchain_openai import AzureChatOpenAI\n",
    "from dotenv import find_dotenv, load_dotenv\n",
    "import os\n",
    "from IPython.display import Markdown # Pretty output\n",
    "\n",
    "\n",
    "# Get environment variables\n",
    "load_dotenv(find_dotenv(), override=True)\n",
    "\n",
    "\n",
    "llm = AzureChatOpenAI(\n",
    "    azure_deployment=os.getenv(\"AZURE_OPENAI_DEPLOYMENT_GPT_4O_MINI\"),\n",
    "    model=os.getenv(\"OPENAI_MODEL_GPT_4O-MINI\", default=\"gpt-4o-mini\"),\n",
    "    temperature=0,\n",
    ")\n",
    "\n",
    "reasoning_llm = AzureChatOpenAI(\n",
    "    azure_deployment=os.getenv(\"AZURE_OPENAI_DEPLOYMENT_O4_MINI\"),\n",
    "    model=os.getenv(\"OPENAI_MODEL_O4-MINI\", default=\"o4-mini\"),\n",
    "    reasoning_effort=\"medium\",\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Test modellen"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Generate the prompt\n",
    "prompt = 'Hei!'\n",
    "\n",
    "# Send the prompt and recieve a response\n",
    "response = llm.invoke(prompt)\n",
    "\n",
    "# Show the response from the model\n",
    "response\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Show only the content of the response\n",
    "response.content"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Lek deg rundt med temperatur-parameteren"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Test out the temperature-parameter\n",
    "\n",
    "# Define the prompt\n",
    "prompt = 'Hei! Fortell meg en morsom historie om en katt. Skriv maks 5 setninger.'\n",
    "\n",
    "# Set temperature (0 = most predictable, 1 = most creative and random)\n",
    "temperature = 0.9  # Try to switch this out with 0.1 and 0.5 to see the difference. \n",
    "\n",
    "# Send the prompt and receive a response\n",
    "response = llm.invoke(prompt, temperature=temperature)\n",
    "\n",
    "# Print the response from the model\n",
    "print(response.content)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Hvordan lage en enkel AI-kjede med LangChain Expression Language (LCEL)\n",
    " \n",
    "LCEL er en metode for å bygge og kjøre såkalte *kjeder* i LangChain på. Kjeder, eller chains, brukes for å koble sammen ulike AI-komponenter. F.eks. kan språkmodeller, datakilder og logikk kobles sammen til én sammenhengende prosess, dvs. en kjede. LCEL gir et standardisert språk for å definere disse kjedene, og er brukervennlig fordi man slipper å lage alt manuelt med kode. Med andre ord får du en \"oppskrift\" på hvordan AI-komponentene dine skal jobbe sammen.\n",
    " \n",
    "**Fordelene med LCEL**\n",
    "1. Støtter parallell og asynkron kjøring - Ulike deler av kjeden kan kjøre samtidig, og systemet kan behandle flere forespørsler på en gang. Dermed kan oppgaver behandles raskere.\n",
    "2. Strømming av resultater - Man kan begynne å se svar mens AI-en fremdeles jobber. (Passer spesielt godt for chatbaserte løsninger)\n",
    "3. Enkel feilsøking med LangSmith - Når kjedene blir komplekse er det viktig å kunne se hva som har blitt gjort underveis. LCEL logger automatisk alt til LangSmith, som gjør det enklere å feilsøke.\n",
    "4. Standardisert - Alle kjeder i LCEL bruker samme grensesnitt, som gjør dem enkle å kombinere og gjenbruke på tvers av prosjekter.\n",
    " \n",
    "LCEL bruker en pipe-operator (|) til å koble sammen ulike trinn i kjeden. Den tar ut data fra én komponent og sender den direkte som input til neste komponent. LCEL bruker også PromptTemplate, som kan tenkes på som en mal for teksten du sender til språkmodellen. PromptTemplate gjør det enkelt å lage dynamiske meldinger ved at man kan sette inn variabler i teksten, litt som en oppskrift der du fyller inn det som mangler før det sendes til AI-modellen. Fordelen med PromptTemplate er at man kan lage én mal, og bruke den med ulike data. Det hjelper deg også med å skille selve teksten fra logikken, og kan gjøre prosessen sikrere ved at man unngår feil som kan oppstå ved manuell string-manipulasjon. Vi skal nå se på noen eksempler med LCEL som bruker pipe-operator og PromptTemplate.\n",
    " \n",
    "Kilde: xxx\n",
    " "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_core.prompts import PromptTemplate\n",
    "prompt = PromptTemplate.from_template(\n",
    "    \"\"\"\n",
    "    Hi! Please talk like a {role}.\n",
    "    \"\"\"\n",
    ")\n",
    "prompt"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "chain = prompt | llm\n",
    "\n",
    "chain.invoke({\"role\": \"pirate\"})"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Som dere ser returnerer modellen mye mer enn kun svaret på prompten vår, nemlig en hel haug med meta-data. Ofte er man kun interessert i tekst-responsen fra modellen og da kan man bruke StrOutputParser for å redusere den unødvendige støyen."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_core.output_parsers import StrOutputParser\n",
    "chain_str = chain | StrOutputParser()\n",
    "\n",
    "chain_str.invoke({\"role\": \"pirate\"})\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Prøv selv - Bygg din egen mal\n",
    "Finn på en prompt med variabler som f.eks\n",
    "- Finner antall inbyggere i land X i år Y\n",
    " - Finner forventet levealder på hunderase X\n",
    " - Gir modellen en rolle X (assistent, detektiv, lege etc) og ber den forklare deg konsept Y\n",
    " - Gjør noe helt sykt, her setter kun fantasien din grenser!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# PromptTemplate as an email-template. \n",
    "\n",
    "prompt = PromptTemplate.from_template(\n",
    "    \"\"\"\n",
    "    Your prompt :)\n",
    "    \"\"\"\n",
    ")  \n",
    "\n",
    "chain = prompt | llm\n",
    "chain_str = chain | StrOutputParser()\n",
    "\n",
    "chain_str.invoke({\"variabel_1\":...}) # Fill out the blanks!"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Kategorisering av hver enkelt tilbakemelding"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Bruk LLMen til å kategorisere tilbakemeldingene\n",
    "I første omgang av kategoriseringen er vi interesserte i å se hvor mange av tilbakemeldingene som passer innenfor de kategoriene ledelsen foreslo, nemlig Network, Training og IT-support. Tilbakemeldinger modellen mener at ikke passer i noen av disse forhåndsbestemte kategoriene vil vi samle opp i en 'Other' kategori."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "categorize_prompt = PromptTemplate.from_template(\n",
    "\"\"\"\n",
    "Categorize the following feedback into one of the following categories:\n",
    "- Network\n",
    "- Training\n",
    "- IT-support\n",
    "- Other\n",
    "\n",
    "Feedback:\n",
    "<feedback>\n",
    "{feedback}\n",
    "</feedback>\n",
    "\"\"\"\n",
    ")\n",
    "\n",
    "categorize_chain = categorize_prompt | llm | StrOutputParser()\n",
    "\n",
    "categorize_chain.invoke({\"feedback\": \"I am very happy with the IT support I received last week.\"})"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Datavalidering og struktur"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Pydantic \n",
    "Pydantic er et Python-bibliotek for datavalidering og datastrukturering. Hovedklassen i Pydantic heter BaseModel og er klassen vi arver fra når vi lager våre egne oppsett. Gjennom BaseModel får vi automatisk funksjonalitet som kan:\n",
    "1. Validere innhold du sender inn\n",
    "    - Eks: Du definerer en liste med godkjente land, \"Norge, Sverige, Finland\", da vil ikke pydantic godkjenne \"Australia\".\n",
    "2. Konvertere data til riktig type\n",
    "    - Eks: Du definerer at output skal være en int og sender inn '1', pydantic vil da returnere 1 (som int)\n",
    "3. Påtvinge JSON-formatering\n",
    "    - Sørger for at responsen LLMen gir matcher JSON-skjemaet til Pydantic. Dette gjør at vi kan være sikre på strukturen til outputen, som for eksempel er svært nyttig om vi ønsker å bruke outputen fra en modell som input i en annen. \n",
    "\n",
    "\n",
    "\n",
    "## Structured output \n",
    "I denne konteksten refererer structured output til strategien og verktøyene vi bruker for å forsikre oss om at dataen vår blir organisert på en måte vi definerer på forhånd. \n",
    "Funksjonalitet i Pydantic biblioteket lar oss bestemme strukturen på outputen gjennom å spesifisere felter, definere typer (eks. int, str, List[int]) og validere data.\n",
    "I dette kurset kommer vi til å bruke BaseModel til å lage våre egne klasser for å sikre at det vi mottar fra modellene når vi prompter de kommer på akkurat den formen vi ønsker. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from pydantic import BaseModel, Field\n",
    "\n",
    "class Categorize(BaseModel):\n",
    "    \"Categorization of a single feedback entry from an IT survey.\"\n",
    "    category : str = Field(description=\"The best fitting category. Only one.\") \n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "I eksempelet over har vi laget vår egen klasse 'Categorize' som arver av BaseModel.\n",
    "For variabelen category har vi brukt type hinting ( x: type ) for å definere typen Pydantic skal forvente at category er lik. Dette for ekesempel blir da ulovlig"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "Categorize(category=1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Mens dette er helt ok"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "Categorize(category=\"Network\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Vi bruker også funksjonen Field, denne kan du bruke til å sette standardverdier, valideringsregler og beskrivelser.\n",
    "I dette kurset bruker vi kun beskrivelse, men for de spesielt interesserte kan dere lese mer om funksjonaliteten [her](https://docs.pydantic.dev/latest/concepts/fields/).\n",
    "\n",
    "For å få LLMen til å skjønne at den må følge reglene vi har definert i klassen vår bruker vi wrapperen with_structured_output(...). \n",
    "Denne wrapper kallet vårt til språkmodellen med logikk som forsikrer at outputen følger strukturen vi har definert i klassen."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "categorize_chain_structured_output = categorize_prompt | llm.with_structured_output(\n",
    "    Categorize,\n",
    "    method=\"json_schema\", # Demands JSON-schema for output\n",
    "    strict=True           # The model must adhere to the specified schema, no extra fields or missing fields are allowed. All types must be an exact match. \n",
    ")\n",
    "\n",
    "categorize_chain_structured_output.invoke(\n",
    "    {\"feedback\": \"I am very happy with the IT support I received last week.\"}\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Vi kan bruke Literal for å definere våre egne typer til å bruke for type hinting. Vi kan type hinte variabler med CATEGORIES under og dette vil da modellen tolke på samme måte som at en int bare har lov til å være et heltall har denne \"typen\" bare lov til å være en av verdiene listet opp i Literal-objektet. Med andre ord får ikke modellen returnert noen andre kategorier enn de som er definert i lista. \n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from typing import Literal\n",
    "\n",
    "CATEGORIES = Literal[\n",
    "    \"Network\",\n",
    "    \"Training\",\n",
    "    \"IT-support\",\n",
    "    'Other'\n",
    "]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La oss sette sammen det vi har lært. Legg merke til at vi ikke lenger lister kategoriene i prompten (som på ingen måte garanterer at vi kun får ut en av kategorierne vi øsnker), men definerer de som et krav til strukturen på outputen fra modellen. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from typing import Literal\n",
    "\n",
    "categorize_prompt = PromptTemplate.from_template(\n",
    "\"\"\"\n",
    "Categorize the following feedback into the provided categories.\n",
    "\n",
    "Feedback:\n",
    "<feedback>\n",
    "{feedback}\n",
    "</feedback>\n",
    "\"\"\"\n",
    ")\n",
    "\n",
    "\n",
    "\n",
    "class CategorizeFromOptions(BaseModel):\n",
    "    \"Categorization of a single feedback entry from an IT survey.\"\n",
    "    category: CATEGORIES = Field(\n",
    "        description=\"Chosen category for the feedback. Choose 'Other' if the other categories provided are not a good fit.\"  \n",
    "    )\n",
    "\n",
    "\n",
    "categorize_chain_structured_output = categorize_prompt | llm.with_structured_output(\n",
    "    CategorizeFromOptions,\n",
    "    method=\"json_schema\",\n",
    "    strict=True,\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "result = categorize_chain_structured_output.invoke(\n",
    "    {\"feedback\": \"I am very happy with the IT support I received last week.\"}\n",
    ")\n",
    "\n",
    "result\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Chain-of-thought\n",
    " \n",
    "Chain of Thought (CoT) er en teknikk innen prompt engineering som hjelper språkmodeller med å løse oppgaver som krever flere tankesteg. I stedet for å hoppe rett til svaret, blir modellen ledet gjennom en logisk og trinnvis prosess som kan gi mer presise og gjennomtenkte svar [1].\n",
    " \n",
    "Du kan altså be modellen om å \"tenke høyt\" under oppgaven og forklare stegene sine før den leverer et endelig svar. Dette ber du om i prompten som sendes inn.\n",
    " \n",
    "Eksempel på en prompt **uten** CoT:\n",
    " \n",
    "    Prompt: \"Hvor mange armer har Eline og Kaspara?\"\n",
    " \n",
    "    Svar: \"4\"\n",
    " \n",
    "Eksempel på en prompt **med** CoT:\n",
    " \n",
    "    Prompt: \"Hvor mange armer har ELine og Kaspara? Tenk trinn for trinn.\"\n",
    " \n",
    "    Svar: \"En person har to armer. To personer betyr 2x2 = 4 armer. Svaret er 4.\"\n",
    " \n",
    "Det kan være fordelaktig å bruke CoT når man jobber med komplekse oppgaver, da nøyaktigheten på outputet fra modellen øker når den \"får lov\" til å jobbe seg gjennom problemet.\n",
    " \n",
    "I tillegg kan du se hvordan modellen tenker, som gjør det lettere for deg å evaluere svaret. Det blir også lettere å se hvor det gikk galt hvis modellen svarer feil.\n",
    " \n",
    "##### *Kilder*\n",
    "[1] [IBM](https://www.ibm.com/think/topics/chain-of-thoughts)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Eksempel: CoT vs. non-CoT"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class CategorizeCot(BaseModel):\n",
    "    \"Categorization of a single feedback entry from an IT survey.\"\n",
    "    chain_of_thought: str = Field(\n",
    "        description=\"Use this space to think through the categorization.\"  # We have defined a variable that explicitly asks the model to think in chain-of-thought\n",
    "    )\n",
    "    category: CATEGORIES = Field(\n",
    "        description=\"Chosen category for the feedback. Choose 'Other' if the other categories provided are not a good fit.\"  \n",
    "    )\n",
    "\n",
    "\n",
    "categorize_chain_cot = categorize_prompt | llm.with_structured_output(\n",
    "    CategorizeCot,\n",
    "    method=\"json_schema\",\n",
    "    strict=True,\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "result = categorize_chain_cot.invoke(\"The internet-speed is so slow I could chase after the IT-survice guy with a baseball-bat!\")\n",
    "print(result.chain_of_thought) \n",
    "print(result.category)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Resonneringsmodell"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Resonneringsmodeller, som Azure Open AI sin O4-mini-modell, er språkmodeller som er spesielt trent på å tenke før de svarer. Altså *resonnerer* de seg frem til et svar, i motsetning til en typisk LLM som gir raske svar.\n",
    "Resonneringsmodeller bruker chain of thought til å bryte ned oppgaven i mindre deler, for å så jobbe seg gjennom problemstillingen stegvis [3]. Dette er fordelaktig å bruke til oppgaver som krever kompleks problemløsning, logisk tenkning som koding eller matematikk eller til oppgaver med flere steg. Det vil også være fordelaktig å bruke i situasjoner der nøyaktighet og forklarbarhet er viktig [2].\n",
    " \n",
    " \n",
    "##### Kilder\n",
    "[OpenAI](https://platform.openai.com/docs/guides/reasoning?api-mode=chat)\n",
    "\n",
    "[AIavisen](https://aiavisen.no/resonneringsmodeller/)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Eksempel på ressonering vs. ikke-ressonering"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Example\n",
    "\n",
    "prompt = PromptTemplate.from_template(\"\"\"\n",
    "A rope ladder hangs over the side of a boat. The rungs are 30 cm apart. The tide rises 1 meter every hour. After 3 hours, how many rungs will be underwater?\n",
    "\"\"\")\n",
    "\n",
    "reasoning_chain = prompt | reasoning_llm | StrOutputParser()\n",
    "\n",
    "print(reasoning_chain.invoke({}))\n",
    "\n",
    "none_reasoning_chain = prompt | llm | StrOutputParser()\n",
    "\n",
    "print(none_reasoning_chain.invoke({}))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Aha! Her ser vi et eksempel på at resonneringsmodellen tenker riktig fordi den bryter ned problemet, mens det vanlige llm-kallet uten resonnering går fem på lureoppgaven og svarer feil."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Nå lar vi modellen gå løs på hele datasettet"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Nå skal vi bruke verktøyene vi har lært til å utføre den første oppgaven, nemlig å kategorisere tilbakemeldingene på tvers av datasettet. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def categorize_single_feedback(feedback: str) -> str:       # Function to invoke model\n",
    "    result = categorize_chain_structured_output.invoke(\n",
    "        {\"feedback\": feedback}\n",
    "    )                                       \n",
    "    return result.category\n",
    "\n",
    "df[\"AI Classification\"] = df[\"Feedback\"].apply(\n",
    "    lambda feedback: categorize_single_feedback(feedback)   # Applies function to all rows in dataset\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Nå vil vi se nærmere på radene som falt under 'Other' og ikke ble kategorisert i første omgang. Til dette vil vi bruke en resonneringsmodell. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Categorize_others(BaseModel):\n",
    "    \"Categorization of a single feedback entry from an IT survey.\"\n",
    "    category_take1 : str = Field(\n",
    "        description=\"The best fitting category. Only one.\"\n",
    "    )\n",
    "    category_take2 : str = Field(\n",
    "        description = \"Do another take of the categorization. If the category is very similar to any of the predetermined categories 'Network','Training', 'IT-support' then rather use one of those\"\n",
    "    )\n",
    " \n",
    " \n",
    "categorize_prompt_other = PromptTemplate.from_template(\n",
    "\"\"\"\n",
    "Categorize the following feedback from an IT-survey into the category that best describes the feedback.\n",
    " \n",
    "Feedback:\n",
    "<feedback>\n",
    "{feedback}\n",
    "</feedback>\n",
    "\"\"\"\n",
    ")\n",
    " \n",
    "categorize_chain_structured_output_others = categorize_prompt_other | reasoning_llm.with_structured_output(\n",
    "    Categorize_others,\n",
    "    method=\"json_schema\",\n",
    "    strict=True,\n",
    ")\n",
    " \n",
    "# Retrieve indexes for all rows where AI classification = \"Other\"\n",
    "other_indices = df[df['AI Classification'] == \"Other\"].index\n",
    " \n",
    " \n",
    "result_batch = categorize_chain_structured_output_others.batch(\n",
    "    [\n",
    "    {\"feedback\": feedback} for feedback in df.loc[other_indices, \"Feedback\"]\n",
    "    ]\n",
    ")\n",
    " \n",
    "df.loc[other_indices, \"AI Classification\"] = [result.category_take2 for result in result_batch]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df\n",
    "\n",
    "# Per nå bruker den kategoriene vi fikk fra LLMen. Når den kategorieserer Other får vi mange nye og litt varierende kategorier. Det er også mange av de. Skulle vi bare brukt den som eksempel også bruke den ekte kategoriseringe her?"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Oi! Her fikk vi mange andre kategorier. Du kan sammenligne AI sin kategorisering med fasiten i kolonnen \"Category\" - reflekter litt rundt hvorvidt AI er for spesifik. Her kan f.eks. prompten skrives om for å få mer / mindre spesifikke kategorier. Kunne vi definert en til kategori som \"eksempel\" for modellen for å unngå så mange kategorier med små variasjoner."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Store the results from the categorization of \"Others\" in a separate variable that is not part of the actual categories.\n",
    "# This can then be used in the tasks below without confusing the LLM about which categories it should recognize.\n",
    "\n",
    "categorized_survey = df[['ID','Feedback','AI Classification']]\n",
    "categorized_survey"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Nå har vi definert passende kategorier for alle tilbakemeldingene. \n",
    "Videre vil vi få modellen til å oppsummere per kategori, slik at vi sitter igjen med en overordnet oversikt. "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Din tur!\n",
    "\n",
    "Nå som kategoriseringen er gjennomført, og vi har forsikret oss om at alle radene har fått en passende kategori, kan vi gå videre til neste oppgave. Her er målet å samle informasjonen fra kategoriseringen til en overordnet oversikt. Lag en LCEL-kjede som tar resultatet fra forrige oppgave og oppsummerer per kategori ved hjelp av en LLM. Inkluder structured output. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Tip 1: Create a good prompt! Use this to reflect on what the end goal is.\n",
    "prompt = PromptTemplate.from_template(\"\"\"\n",
    "Well, how should I phrase this? Shouldn't there be some data included here as well?\n",
    "\"\"\")\n",
    "\n",
    "# Tip 2: Can you create a class that inherits from BaseModel to make this easier?\n",
    "class SummarizeFeedback(BaseModel):\n",
    "    \"Description...\"\n",
    "    summary: str = ...\n",
    "\n",
    "# Tip 3: Time to create the chain\n",
    "summary_chain_structured_output = ...\n",
    "\n",
    "# Tip 4: Call the model with the dataset from the survey\n",
    "summary = ..."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "\n",
    "# Fasit\n",
    "# 1. Prompt Template\n",
    "summary_prompt = PromptTemplate.from_template(\"\"\"\n",
    "You are a domain expert in internal IT operations and organizational analysis. You will be provided with a dataset containing qualitative feedback from employees in an IT company. \n",
    "Each row in the dataset represents a feedback entry and is associated with a specific category.\n",
    "\n",
    "For each category, carefully:\n",
    "1. Read and interpret the feedback entries assigned to that category.\n",
    "2. Identify core themes, recurring patterns, and contrasting opinions within that category.\n",
    "3. Evaluate the feedback logically: What are the likely underlying causes of recurring issues or praises? Are there signs of systemic problems, isolated incidents, or misaligned expectations?\n",
    "4. Summarize each category in 3 to 6 bullet points, highlighting key sentiments (positive and negative), representative concerns or compliments, and any significant outliers\n",
    "\n",
    "Present your findings in a clean, professional way with one section per category. \n",
    "\n",
    "This is the employee feedback data: {survey_results}\n",
    "\"\"\")\n",
    "\n",
    "# 2. Class for structured output\n",
    "class SummarizeFeedback(BaseModel):\n",
    "    \"Summary of the different categorizes recognized in the feedback from an IT-survey.\"\n",
    "    summary : str = Field(\n",
    "        description=\"For each category: Category name and 3-6 bullet points summarizing the category.\"\n",
    "    )\n",
    "\n",
    "# 3. Summary-chain\n",
    "summary_chain_structured_output = summary_prompt | llm.with_structured_output(\n",
    "    SummarizeFeedback,\n",
    "    method=\"json_schema\",\n",
    "    strict=True,\n",
    ")\n",
    "\n",
    "# 4. Kall modellen med det kategoriserte datasettet fra undersøkelsen \n",
    "summary = summary_chain_structured_output.invoke({\"survey_results\": categorized_survey})\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# A nice way to show the output from the model\n",
    "from IPython.display import Markdown\n",
    "\n",
    "display(Markdown(summary.summary))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Rapport til ledelsen\n",
    "\n",
    "I siste oppgave skal vi bruke oppsummeringen vi nettopp lagde til å generere en rapport som kan sendes til ledelsen. Rapporten skal gi mottakerne en oversikt over de viktigste punktene i medarbeideundersøkelsen og komme med noen forslag til endringer som kan skape forbedringer til neste år. \n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Oppgave \n",
    "report_prompt = PromptTemplate.from_template(\"\"\"\n",
    "Bla bla bla\n",
    "\"\"\")\n",
    "\n",
    "# 2. Class for structured output\n",
    "class ReportForLeadership(BaseModel):\n",
    "    \"Raport for the leadership of an IT-company on results of an internal IT-survey.\"\n",
    "    snappy_title : str = Field(\n",
    "        description=\"A fitting title for the report. Must begin with '# ' to ensure easy markdown formatting.\"\n",
    "    )\n",
    "    key_takeaway:str = ...\n",
    "\n",
    "# 3. Report-chain\n",
    "report_chain_structured_output = ...\n",
    "\n",
    "# 4. Kall modellen med oppsummeringen av kategoriene\n",
    "report = ...\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Fasit\n",
    "report_prompt = PromptTemplate.from_template(\"\"\"\n",
    "You are an expert HR and technical operations analyst. I will provide you with a dataset of employee feedback collected from an IT company.\n",
    "\n",
    "Your task is to deeply analyze this feedback and generate a concise executive-level summary report in markdown format that includes:\n",
    "\n",
    "1. Key Takeaways\n",
    "Provide a short summary of the overall feedback in 3-5 bullet points. Focus only on the main issues or areas of satisfaction.\n",
    "Include both positive and negative themes, but prioritize the most important and impactful points.\n",
    "Limit each point to 1-2 sentences.\n",
    "Before finalizing each point, take a moment to reflect on why each issue might be present (e.g., systemic problems, temporary issues, resource constraints, etc.)\n",
    "\n",
    "2. Suggested Improvements\n",
    "Based on the overall feedback, propose 2-3 high-level, actionable measures that the company could take to address the most pressing issues and enhance overall performance or satisfaction.\n",
    "Each suggestion should be brief, directly tied to the feedback, and strategic in nature.\n",
    "Think about short-term vs long-term solutions and consider the feasibility of each suggestion.\n",
    "\n",
    "3. Output\n",
    "Present your findings in a structured way with clear section headings, bullet points for easy scanning, and a consise, direct and professional tone suitable for leadership review.\n",
    "\n",
    "This is the employee feedback data: {summary_text}\n",
    "\"\"\")\n",
    "\n",
    "# 2. Class for structured output\n",
    "class ReportForLeadership(BaseModel):\n",
    "    \"Raport for the leadership of an IT-company on results of an internal IT-survey.\"\n",
    "    snappy_title : str = Field(\n",
    "        description=\"A fitting title for the report. Must begin with '# ' to ensure easy markdown formatting.\"\n",
    "    )\n",
    "    intro : str = Field(\n",
    "        description=\"1 sentence describing thepurpose of the report.\" \n",
    "    )\n",
    "    key_takeaways : str = Field(\n",
    "        description=\"3-5 bulletpoints describing the key-takeaways. Limit each point to 1-2 sentences.\" \n",
    "    )\n",
    "    suggested_improvements : str = Field(\n",
    "        description=\"2-3 actionable measures for the company. Keep it brief.\"\n",
    "    )\n",
    "    outro: str = Field(\n",
    "        description=\"1 sentence ending for the report. Be creative.\" \n",
    "    )\n",
    "\n",
    "# 3. Report-chain\n",
    "report_chain_structured_output = report_prompt | llm.with_structured_output(\n",
    "    ReportForLeadership,\n",
    "    method=\"json_schema\",\n",
    "    strict=True,\n",
    ")\n",
    "\n",
    "# 4. Kall modellen med oppsummeringen av kategoriene\n",
    "report = report_chain_structured_output.invoke({\"summary_text\": summary.summary})\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "report"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "display(Markdown(\n",
    "    \"\\n\\n\".join([report.snappy_title,\n",
    "                 report.intro,\n",
    "                 '## Key-takeaways',report.key_takeaways,\n",
    "                 '## Suggested improvements',report.suggested_improvements,\n",
    "                 report.outro])\n",
    "                 \n",
    "))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Fantastisk! \n",
    "Du har nå fullført kurset *LLM for alle*, og jobbet deg gjennom hvordan du kan bruke metoder og verktøy som LCEL og structured output til å....\n",
    "For de aller ivrigste har vi laget en ekstraoppgave helt til slutt - prøv deg frem hvis du har tid!"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Ekstraoppgave: Hvordan kunne vi gjort dette bedre?\n",
    "\n",
    "Anta at du har fått levert resultatene fra denne spørreundersøkelsen i fanget av en stressa mellomleder som ber deg levere en rapport han kan presentere ledelsen. \n",
    "Gitt verktøyene du har fått en innføring i gjennom dette kursene (og kanskje andre erfaringer?), hvordan ville du løst oppgaven?\n",
    "\n",
    "Ser du for eksempel noe som kunne vært forbedret i\n",
    "- Rekkefølgen på måten vi leter etter kategorier?\n",
    "- Legger vi får mye/lite vekt på inputen vi fikk om hva ledelsen \"tror\" kategoriene kommer til å være?\n",
    "- Promptingen?\n",
    "- Variablene eller type hintingen i pydantic-klassene?\n",
    "\n",
    "Ville du kanskje gjort det helt annerledes? \n",
    "Now's your chance to try!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Prøv deg frem :))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Hva er risikoene ved bruk av LLM til denne typen analyse?\n",
    "Ved bruk av store språkmodeller (LLM) til analyse av åpne tekstsvar i spørreundersøkelser er risikoen generelt lavere enn i mer kritiske anvendelser som faktagenerering eller beslutningsstøtte. Likevel finnes det enkelte utfordringer som bør tas på alvor for å sikre kvalitet og transparens i analysen.\n",
    " \n",
    "Skjevheter og bias\n",
    "LLM-er kan tolke og vekte visse formuleringer eller temaer ulikt basert på mønstre fra treningsdataene. Dette kan føre til at noen svar over- eller underrepresenteres i visse kategorier avhengig av hvordan modellen \"forstår\" teksten. For eksempel kan emosjonelt ladede eller språklig avanserte svar få uforholdsmessig stor plass, mens mer nøkterne eller utypiske svar blir feilklassifisert. Særlig ved tolkning av følsomme temaer er det viktig å være oppmerksom på disse tendensene.\n",
    " \n",
    "Hallusinasjon – mindre relevant, men ikke fraværende\n",
    "I denne typen oppgave, hvor modellen hovedsakelig skal kategorisere eller oppsummere eksisterende tekstsvar er risikoen for såkalte hallusinasjoner (altså at modellen «finner opp» informasjon) mindre, men ikke helt ubetydelig. Det kan forekomme at modellen overfortolker et kort eller tvetydig svar og tillegger det en mening som ikke eksplisitt finnes i teksten. Dette kan føre til at enkelte svar havner i feil kategori eller påvirker tolkningen av sentiment eller tematikk.\n",
    " \n",
    "Behov for kvalitetssikring\n",
    "Selv med høy presisjon og effektivitet bør man ikke ta modellens resultater for gitt. Det anbefales å gjennomføre stikkprøver, sammenligne med manuell koding og evaluere nøyaktigheten på klassifiseringene. Det kan være veldig nyttig å kombinere automatisk kategorisering med menneskelig vurdering både i startfasen og underveis."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": ".venv",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.13.0"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
