diff --git a/ai/generative-ai-service/hr-goal-alignment/.gitignore b/ai/generative-ai-service/hr-goal-alignment/.gitignore new file mode 100644 index 000000000..d69ef29a7 --- /dev/null +++ b/ai/generative-ai-service/hr-goal-alignment/.gitignore @@ -0,0 +1,8 @@ +**/__pycache__/ +venv/ +files/venv/ +**/config.py +.DS_Store +data/ +*.log +*.txt diff --git a/ai/generative-ai-service/hr-goal-alignment/LICENSE.txt b/ai/generative-ai-service/hr-goal-alignment/LICENSE.txt new file mode 100644 index 000000000..8dc7c0703 --- /dev/null +++ b/ai/generative-ai-service/hr-goal-alignment/LICENSE.txt @@ -0,0 +1,35 @@ +Copyright (c) 2025 Oracle and/or its affiliates. + +The Universal Permissive License (UPL), Version 1.0 + +Subject to the condition set forth below, permission is hereby granted to any +person obtaining a copy of this software, associated documentation and/or data +(collectively the "Software"), free of charge and under any and all copyright +rights in the Software, and any and all patent rights owned or freely +licensable by each licensor hereunder covering either (i) the unmodified +Software as contributed to or provided by such licensor, or (ii) the Larger +Works (as defined below), to deal in both + +(a) the Software, and +(b) any piece of software and/or hardware listed in the lrgrwrks.txt file if +one is included with the Software (each a "Larger Work" to which the Software +is contributed by such licensors), + +without restriction, including without limitation the rights to copy, create +derivative works of, display, perform, and distribute the Software and make, +use, sell, offer for sale, import, export, have made, and have sold the +Software and the Larger Work(s), and to sublicense the foregoing rights on +either these or other terms. + +This license is subject to the following condition: +The above copyright notice and either this complete permission notice or at +a minimum a reference to the UPL must be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/ai/generative-ai-service/hr-goal-alignment/README.md b/ai/generative-ai-service/hr-goal-alignment/README.md new file mode 100644 index 000000000..d4489713d --- /dev/null +++ b/ai/generative-ai-service/hr-goal-alignment/README.md @@ -0,0 +1,35 @@ +# HR Goal Alignment Chatbot + +This project provides an AI-powered HR assistant suite built using Oracle Cloud Infrastructure's Generative AI, Oracle Database, and Streamlit. + +This asset lives under: `ai/generative-ai-service/hr-goal-alignment` + +--- + +## When to use this asset? + +This chatbot system is designed to support employee–manager goal alignment, self-assessment reflection, course recommendations, and meeting preparation through conversational AI. It’s especially useful during performance review cycles and organizational alignment phases. + +--- + +## How to use this asset? + +See the full setup and usage guide in [`files/README.md`](./files/README.md). + +--- + +## License + +Copyright (c) 2025 Oracle and/or its affiliates. + +Licensed under the Universal Permissive License (UPL), Version 1.0. + +See [LICENSE](./LICENSE) for more details. + +--- + + + +## Disclaimer + +ORACLE AND ITS AFFILIATES DO NOT PROVIDE ANY WARRANTY WHATSOEVER, EXPRESS OR IMPLIED, FOR ANY SOFTWARE, MATERIAL OR CONTENT OF ANY KIND CONTAINED OR PRODUCED WITHIN THIS REPOSITORY, AND IN PARTICULAR SPECIFICALLY DISCLAIM ANY AND ALL IMPLIED WARRANTIES OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY, AND FITNESS FOR A PARTICULAR PURPOSE. FURTHERMORE, ORACLE AND ITS AFFILIATES DO NOT REPRESENT THAT ANY CUSTOMARY SECURITY REVIEW HAS BEEN PERFORMED WITH RESPECT TO ANY SOFTWARE, MATERIAL OR CONTENT CONTAINED OR PRODUCED WITHIN THIS REPOSITORY. IN ADDITION, AND WITHOUT LIMITING THE FOREGOING, THIRD PARTIES MAY HAVE POSTED SOFTWARE, MATERIAL OR CONTENT TO THIS REPOSITORY WITHOUT ANY REVIEW. USE AT YOUR OWN RISK. diff --git a/ai/generative-ai-service/hr-goal-alignment/files/Org_Chart.py b/ai/generative-ai-service/hr-goal-alignment/files/Org_Chart.py new file mode 100644 index 000000000..1bd37bce1 --- /dev/null +++ b/ai/generative-ai-service/hr-goal-alignment/files/Org_Chart.py @@ -0,0 +1,186 @@ +# Copyright (c) 2025 Oracle and/or its affiliates. +import streamlit as st +from urllib.parse import quote, unquote + +from org_chart_backend import check_goal_alignment, fetch_goals_from_emp, mapping_all_employees + +try: + import graphviz +except ImportError: + st.error("Please install graphviz package: pip install graphviz") + st.stop() + +st.title("Organization Chart") +st.markdown("Use the sidebar to adjust settings and select employees for details.") + +# Get org chart data +mapping, employees_df = mapping_all_employees() +if not mapping: + st.warning("No data available to display.") + st.stop() + +# Get all employee names +all_employees = sorted(set(mapping.keys()) | {emp for values in mapping.values() for emp in values}) + +# Sidebar controls +with st.sidebar: + st.header("Chart Settings") + orientation = st.selectbox("Layout", ["Top-Down", "Left-Right"], index=0) + zoom_level = st.slider("Zoom Level (%)", 50, 200, 50, step=10) + + # Direct employee selection + st.markdown("---") + st.header("Select Employee") + + # Preloaded selectbox with all employees + selected_employee = st.selectbox( + "Choose an employee:", + [""] + all_employees, # Add empty option at the top + index=0 + ) + + if selected_employee: + if st.button("View Details"): + st.session_state.selected_employee = selected_employee + # Update URL parameter + st.query_params["emp"] = quote(selected_employee) + st.rerun() + +# Store selected employee in session state +if "selected_employee" not in st.session_state: + # Try to get from URL params first + params = st.query_params + emp_param = params.get("emp") + if emp_param: + if isinstance(emp_param, list): + emp_param = emp_param[0] + st.session_state.selected_employee = unquote(emp_param) + else: + st.session_state.selected_employee = None + +# Create the graphviz chart +def create_org_chart(): + dot = graphviz.Digraph(format="svg") + + # Set orientation + if orientation == "Left-Right": + dot.attr(rankdir="LR") + + # Set global attributes + dot.attr("graph") + dot.attr("node", shape="box", style="rounded") + + # Add all nodes + for node in all_employees: + if node == st.session_state.selected_employee: + dot.node(node, style="filled,rounded", fillcolor="#ffd966") + else: + dot.node(node) + + # Add all edges + for manager, subordinates in mapping.items(): + for subordinate in subordinates: + dot.edge(manager, subordinate) + + return dot + +# Create chart +chart = create_org_chart() + +# Generate SVG for zoomable display +svg_code = chart.pipe(format='svg').decode('utf-8') + +# Create HTML with zoom and pan capabilities +html_chart = f""" +
+
+ {svg_code} +
+
+""" + +# Display the chart with HTML component +st.components.v1.html(html_chart, height=520, scrolling=True) # type: ignore + +# Show details section for selected employee +# ------------------------------------------------------------ +# πŸ“‹ Employee profile & goal alignment +# ------------------------------------------------------------ +if st.session_state.selected_employee: + emp = st.session_state.selected_employee + with st.spinner("Loading employee details...", show_time=True): + # ── Fetch data ──────────────────────────────────────────── + manager = next((m for m, emps in mapping.items() if emp in emps), None) + reports = mapping.get(emp, []) + v_align, h_align = check_goal_alignment(employees_df, emp, manager) + goals_df = fetch_goals_from_emp(employees_df, emp) + + # ── PROFILE HEADER ─────────────────────────────────────── + with st.container(border=True): + head, metrics = st.columns([3, 2]) + + with head: + st.header(f"πŸ‘€ {emp.splitlines()[0]}") + title = emp.splitlines()[1][1:-1] if len(emp.splitlines()) > 1 else "" + st.caption(title) + + rel_emp = "" + if manager: + m_name = manager.split('(')[0] + rel_emp = rel_emp + f"**Manager:** {m_name} " + if len(reports) > 0: + rel_emp = rel_emp + f" |  **Direct reports:** {len(reports)}" + + st.write(rel_emp, unsafe_allow_html=True) + + with metrics: + # side-by-side animated metrics + if v_align: + st.metric("Vertical alignment", f"{v_align}% ", f"{100-v_align}%") + if h_align: + st.metric("Horizontal alignment", f"{h_align}% ", f"{100-h_align}%") + + # animated progress bars look slick and let the user + if v_align: + st.progress(v_align / 100.0, text="Vertical goal alignment") + if h_align: + st.progress(h_align / 100.0, text="Horizontal goal alignment") + + # ------------------------------------------------------------ + # 🎯 Goal quality & list (robust, no KeyErrors) + # ------------------------------------------------------------ + SMART_COL = "smart" # original column + LABEL_COL = "Quality" # new column for the emoji chip + + # --- 1. create a Boolean mask ------------------------------- + mask = goals_df[SMART_COL].astype(str).str.lower().isin({"yes", "true", "smart"}) + goals_pretty = goals_df.copy() + goals_pretty[LABEL_COL] = mask.map({True: "βœ… SMART", False: "βšͺ️ Not SMART"}) + goals_pretty.drop(columns=[SMART_COL], inplace=True) + + # move the chip to the front + first = goals_pretty.pop(LABEL_COL) + goals_pretty.insert(0, LABEL_COL, first) + + # --- 2. quick KPI ------------------------------------------- + total, good = len(goals_pretty), mask.sum() + pct_good = int(good / total * 100) if total else 0 + + c_num, c_bar = st.columns([1, 4]) + with c_num: + st.metric("SMART goals", f"{good}/{total}", f"{pct_good}%") + with c_bar: + st.progress(pct_good / 100, text=f"{pct_good}% SMART") + + st.divider() + + # --- 3. style just the chip column -------------------------- + def chip_style(val): + return ( + "background-color:#d4edda;font-weight:bold" + if "βœ…" in val + else "background-color:#f8d7da;font-weight:bold" + ) + + styled = goals_pretty.style.applymap(chip_style, subset=[LABEL_COL]) # type: ignore + st.dataframe(styled, use_container_width=True, hide_index=True) diff --git a/ai/generative-ai-service/hr-goal-alignment/files/README.md b/ai/generative-ai-service/hr-goal-alignment/files/README.md new file mode 100644 index 000000000..f547cf084 --- /dev/null +++ b/ai/generative-ai-service/hr-goal-alignment/files/README.md @@ -0,0 +1,162 @@ +# HR Goal Alignment Chatbot + +This Streamlit-based application demonstrates a modular, AI-powered HR chatbot system designed to help employees and managers align their goals through structured, data-informed conversations. + +The system integrates with Oracle Database and uses OCI's Generative AI models to simulate goal alignment and cascading throughout an organization. + +--- + +## Features + +- Modular chatbot interfaces: + - **Self-Assessment Chatbot**: guides employees in preparing for evaluations + - **Goal Alignment Chatbot**: facilitates alignment of goals with organizational strategy + - **Course Recommendation Chatbot** + - **Manager Meeting Preparation Chatbot** +- Live Oracle Database integration for: + - Self-assessments + - Manager briefings + - Goal sheets +- AI-powered conversations using **Oracle Generative AI** via **LangChain** +- Goal refinement tracking and database updates +- Org chart visualization using **Graphviz** +- Downloadable chat transcripts +- Modular codebase with reusable prompt templates + +--- + +## Project Structure + +```text +. +β”œβ”€β”€ app.py +β”œβ”€β”€ config.py +β”œβ”€β”€ course_vector_utils.py +β”œβ”€β”€ data/ +β”‚ └── Full_Company_Training_Catalog.xlsx +β”œβ”€β”€ data_ingestion_courses.py +β”œβ”€β”€ gen_ai_service/ +β”‚ └── inference.py +β”œβ”€β”€ goal_alignment_backend.py +β”œβ”€β”€ org_chart_backend.py +β”œβ”€β”€ org_chart.py +β”œβ”€β”€ pages/ +β”‚ β”œβ”€β”€ course_recommendation_chatbot.py +β”‚ β”œβ”€β”€ goal_alignment_chatbot.py +β”‚ β”œβ”€β”€ manager_meeting_chatbot.py +β”‚ └── self_assessment_chatbot.py +β”œβ”€β”€ scripts/ +β”‚ β”œβ”€β”€ create_tables.py +β”‚ └── populate_demo_data.py +β”œβ”€β”€ utils.py +β”œβ”€β”€ requirements.txt +└── README.md + +## Setup Instructions + +### 1. Clone the repository +git clone https://github.com/your-username/hr-goal-alignment-chatbot.git +cd hr-goal-alignment-chatbot + +### 2. Create and activate a virtual environment +python3 -m venv venv +On Mac: +source venv/bin/activate +On Windows: +venv\Scripts\activate + +### 3. Install dependencies +pip install -r requirements.txt + +Settingβ€―up Graphviz in your virtual environment +Our Streamlit org‑chart relies on the Graphviz binaries as well as the Python wrapper. +Follow these steps inside the virtual environment where you’ll run the app. + +On Mac: +brew install graphviz +pip install graphviz +Ensure the Graphviz /bin directory is added to your system PATH. +(export PATH="/opt/homebrew/bin:$PATH") + +On Windows: +Download and install Graphviz from https://graphviz.org/download/ +Ensure the Graphviz /bin directory is added to your system PATH. + +### 4. Configure Oracle access +Before running the application, you need to provide your Oracle Cloud Infrastructure (OCI) and Autonomous Database credentials. This entails that you need to have an Oracle Autonomous Database set up in advance in order for this code to work. + +1. Create a config file: + copy the provided template to a new config file + cp config_template.py config.py + +2. Edit config.py and fill in the following: +OCI Settings: + +OCI_COMPARTMENT_ID: Your OCI compartment OCID. +REGION: e.g., eu-frankfurt-1 +GEN_AI_ENDPOINT: Leave as-is unless using a custom endpoint. +AUTH_TYPE: Set to "API_KEY" or "RESOURCE_PRINCIPAL" depending on your deployment. + +Model Settings: + +EMBEDDING_MODEL_ID: Default is a Cohere model. +GENERATION_MODEL_ID: Also Cohere, or replace with your chosen provider. + +Database Credentials: + +DB_USER, DB_PASSWORD: Your ADB login credentials. +DB_DSN: Your TNS-style connection string. You can get this from the Oracle Cloud Console β†’ Autonomous Database β†’ Database Connection β†’ "Wallet Details" β†’ "Connection String". + +GENERATE_MODEL = "ocid1.generativemodel.oc1..xxxxx" +ENDPOINT = "https://inference.generativeai.region.oraclecloud.com" +COMPARTMENT_ID = "ocid1.compartment.oc1..xxxxx" + +### 5. Populate Demo (Optional) +To populate the database with example self-assessment records, manager briefings, and goal sheets: +python scripts/populate_demo_data.py + +### 6. Run the App +To start the chatbot system locally: +streamlit run app.py +You can then navigate to the provided local URL (usually http://localhost:8501) to interact with the available chatbots. + +## Demo Workflow Example +A manager uses the Goal Alignment Chatbot to refine their goals with the help of Oracle Generative AI. +These refinements are tracked and optionally applied to the database. +Next, a direct report uses the same chatbot β€” now aligning their own goals with the updated manager goals. +This demonstrates how alignment cascades hierarchically through the organization. + +Meanwhile, employees can: + +Use the Self-Assessment Chatbot to reflect on performance areas. + +Use the Manager Meeting Preparation Chatbot to view summaries and briefings. + +Get personalized course recommendations aligned to their career goals. + +View the organizational hierarchy using the integrated org chart visualizer. + + +## Tech Stack +Frontend: Streamlit + +Backend: Python + +Data Layer: Oracle Autonomous Database + +AI Orchestration: LangChain + +LLM Provider: Oracle Generative AI (via Cohere or other supported backends) + +Graph Visualization: Graphviz (for org charts) + +## License +Copyright (c) 2025 Oracle and/or its affiliates. + +Licensed under the Universal Permissive License (UPL), Version 1.0. +See the LICENSE file for more details. + +## Disclaimer +ORACLE AND ITS AFFILIATES DO NOT PROVIDE ANY WARRANTY WHATSOEVER, EXPRESS OR IMPLIED, FOR ANY SOFTWARE, MATERIAL OR CONTENT OF ANY KIND CONTAINED OR PRODUCED WITHIN THIS REPOSITORY, AND IN PARTICULAR SPECIFICALLY DISCLAIM ANY AND ALL IMPLIED WARRANTIES OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY, AND FITNESS FOR A PARTICULAR PURPOSE. +FURTHERMORE, ORACLE AND ITS AFFILIATES DO NOT REPRESENT THAT ANY CUSTOMARY SECURITY REVIEW HAS BEEN PERFORMED WITH RESPECT TO ANY SOFTWARE, MATERIAL OR CONTENT CONTAINED OR PRODUCED WITHIN THIS REPOSITORY. +IN ADDITION, AND WITHOUT LIMITING THE FOREGOING, THIRD PARTIES MAY HAVE POSTED SOFTWARE, MATERIAL OR CONTENT TO THIS REPOSITORY WITHOUT ANY REVIEW. USE AT YOUR OWN RISK. \ No newline at end of file diff --git a/ai/generative-ai-service/hr-goal-alignment/files/app.py b/ai/generative-ai-service/hr-goal-alignment/files/app.py new file mode 100644 index 000000000..5e56cd49c --- /dev/null +++ b/ai/generative-ai-service/hr-goal-alignment/files/app.py @@ -0,0 +1,33 @@ +# Copyright (c) 2025 Oracle and/or its affiliates. +import streamlit as st +from pathlib import Path + +st.set_page_config(page_title="HR Chatbots", layout="centered") + +# st.title("HR Chatbots Demo") + +# st.markdown(""" +# Welcome to the HR Chatbots Demo. Use the sidebar to select one of the chatbot assistants: +# - **Goal Alignment Chatbot** Align your goals with your manager and your peers. +# - **Self-Assessment Chatbot** Prepare for your quarterly evaluation. +# - **Manager Meeting Preparation Chatbot** Prepare for your upcoming 1:1 meetings. +# - **Course Recommendation Chatbot** Choose the best course to pursue. + +# Use the sidebar to begin. +# """) + +pages = { + "Organization": [ + st.Page(str(Path("org_chart.py")), title="Org Chart Visualizer"), + ], + "Employee": [ + st.Page(str(Path("pages") / "course_recommendation_chatbot.py")), + st.Page(str(Path("pages") / "goal_alignment_chatbot.py")), + st.Page(str(Path("pages") / "manager_meeting_chatbot.py")), + st.Page(str(Path("pages") / "self_assessment_chatbot.py")), + ], +} + + +pg = st.navigation(pages) +pg.run() diff --git a/ai/generative-ai-service/hr-goal-alignment/files/config_template.py b/ai/generative-ai-service/hr-goal-alignment/files/config_template.py new file mode 100644 index 000000000..925e75d37 --- /dev/null +++ b/ai/generative-ai-service/hr-goal-alignment/files/config_template.py @@ -0,0 +1,47 @@ +# Copyright (c) 2025 Oracle and/or its affiliates. +# config_template.py + +# === OCI Configuration === +OCI_COMPARTMENT_ID = "ocid1.compartment.oc1..your_compartment_id" +OCI_CONFIG_PROFILE = "DEFAULT" +REGION = "your-region-id" # e.g., "eu-frankfurt-1" +GEN_AI_ENDPOINT = f"https://inference.generativeai.{REGION}.oci.oraclecloud.com" + +# === Embedding and Generation Models === +EMBEDDING_MODEL_ID = "cohere.embed-english-v3.0" +GENERATION_MODEL_ID = "cohere.command-r-plus-08-2024" +GENERATION_MODEL_PROVIDER = "cohere" + +# === LLM Parameters === +TEMPERATURE = 0.1 +MAX_TOKENS = 4000 +TOP_P = 0.9 + +# === Vector Store Configuration === +VECTOR_TABLE_NAME = "your_vector_table_name" # e.g., coursera_vector_store +CHUNK_SIZE = 1000 +CHUNK_OVERLAP = 150 + +# === Oracle Autonomous Database Configuration === +# Replace these placeholders with your actual DB credentials and connection string +DB_USER = "your_db_user" +DB_PASSWORD = "your_db_password" +DB_DSN = """ +(description= + (retry_count=20)(retry_delay=3) + (address=(protocol=tcps)(port=1522)(host=your-adb-host.oraclecloud.com)) + (connect_data=(service_name=your_service_name.adb.oraclecloud.com)) + (security=(ssl_server_dn_match=yes)) +) +""" + +# This dict is used for vector store connections +CONNECT_ARGS_VECTOR = { + "user": DB_USER, + "password": DB_PASSWORD, + "dsn": DB_DSN +} + +# === Auth Method === +AUTH_TYPE = "API_KEY" # or "RESOURCE_PRINCIPAL", depending on deployment +CONFIG_PROFILE = OCI_CONFIG_PROFILE diff --git a/ai/generative-ai-service/hr-goal-alignment/files/course_vector_utils.py b/ai/generative-ai-service/hr-goal-alignment/files/course_vector_utils.py new file mode 100644 index 000000000..48d2dc24d --- /dev/null +++ b/ai/generative-ai-service/hr-goal-alignment/files/course_vector_utils.py @@ -0,0 +1,171 @@ +# Copyright (c) 2025 Oracle and/or its affiliates. +from typing import List, Dict, Optional +from langchain_community.vectorstores import OracleVS +import os +import logging +import oci +import pandas as pd +from pathlib import Path +from typing import List + +from langchain_community.vectorstores import OracleVS +from langchain_community.document_loaders import UnstructuredExcelLoader +from langchain_text_splitters import RecursiveCharacterTextSplitter +from langchain_community.embeddings import OCIGenAIEmbeddings +from langchain_core.documents import Document +from langchain_community.vectorstores.utils import DistanceStrategy + +# Use the project's config file +import config +logging.basicConfig( + level=logging.DEBUG, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + handlers=[ + logging.FileHandler("app.log"), # <- write logs into a file called app.log + logging.StreamHandler() # <- also allow printing to the console (terminal) + ] +) +logger = logging.getLogger(__name__) + + +# Configuration will be read from the project's config.py + + +class CourseVectorStore: + def __init__(self, db_connection): + """ + Initializes the CourseVectorStore. + + Args: + db_connection: An active Oracle database connection object. + """ + logger.info("Initializing CourseVectorStore...") + self.db_conn = db_connection + self.embeddings = self._initialize_embeddings() + self.vector_store = self._initialize_vector_store() + logger.info("CourseVectorStore initialized.") + + def _initialize_embeddings(self) -> OCIGenAIEmbeddings: + # Read config from the project's config module + oci_config = oci.config.from_file(profile_name=config.OCI_CONFIG_PROFILE) + return OCIGenAIEmbeddings( + model_id=config.EMBEDDING_MODEL_ID, + service_endpoint=config.OCI_SERVICE_ENDPOINT, + compartment_id=config.OCI_COMPARTMENT_ID, + auth_type="API_KEY", + auth_profile=config.OCI_CONFIG_PROFILE # Use config module + ) + + def _initialize_vector_store(self) -> OracleVS: + # Use the passed-in connection and project config + return OracleVS( + client=self.db_conn, # Use the connection passed in __init__ + embedding_function=self.embeddings, # Use the embeddings initialized in __init__ + table_name=config.VECTOR_TABLE_NAME, # Use table name from project config + distance_strategy=DistanceStrategy.COSINE + ) + + # LLM initialization removed. + # Retrieval logic specific to reports removed. + + def add_courses_from_excel(self, excel_path: str): + """ + Loads course data from an Excel file, processes each row, + and adds it to the Oracle vector store. + """ + logger.info(f"Loading course data from: {excel_path}") + + if not os.path.exists(excel_path): + logger.error(f"Excel file '{excel_path}' not found. Skipping document loading.") + return + + try: + # Use pandas to read structured Excel data (instead of UnstructuredExcelLoader as it was before) + df = pd.read_excel(excel_path) + logger.info(f"Excel file loaded successfully. Number of rows: {len(df)}") + except Exception as e: + logger.error(f"Error reading Excel file {excel_path}: {e}", exc_info=True) + return + + if df.empty: + logger.warning("Excel file is empty. No data to process.") + return + + # Define expected Excel columns and their mapping to metadata keys + column_mapping = { + "Course_id": "COURSE_ID", + "Name": "NAME", + "University": "UNIVERSITY", + "Difficulty Level": "DIFFICULTYLEVEL", + "Rating": "RATING", + "Url": "URL", + "Syllabus": "SYLLABUS", + "Description": "DESCRIPTION", + "Skills": "SKILLS", + } + + processed_docs = [] + for _, row in df.iterrows(): + # Build page content + name = row.get("Name", "N/A") + description = row.get("Description", "N/A") + page_content = f"Course: {name}\nDescription: {description}" + + # Build metadata dictionary + metadata = {} + for excel_col, meta_key in column_mapping.items(): + metadata[meta_key] = row.get(excel_col, "N/A") + + processed_docs.append(Document(page_content=page_content, metadata=metadata)) + + logger.info(f"Created {len(processed_docs)} documents from Excel.") + + if not processed_docs: + logger.warning("No documents processed after row parsing. Check Excel content.") + return + + logger.info(f"Processing {len(processed_docs)} documents for splitting.") + + # Split documents into smaller chunks + text_splitter = RecursiveCharacterTextSplitter( + chunk_size=config.CHUNK_SIZE, + chunk_overlap=config.CHUNK_OVERLAP, + length_function=len, + is_separator_regex=False, + ) + splits = text_splitter.split_documents(processed_docs) + + # Remove duplicates based on page content + unique_chunks = {chunk.page_content: chunk for chunk in splits} + splits = list(unique_chunks.values()) + + if not splits: + logger.warning("No chunks generated. Check if the documents are empty or unsupported.") + return + + try: + self.vector_store.add_documents(splits) + logger.info("Successfully added document chunks to the vector store.") + except Exception as e: + logger.error(f"Error adding document chunks to vector store: {e}", exc_info=True) + + + def similarity_search(self, query: str, k: int = 5) -> List[Document]: + """ + Performs a similarity search against the vector store. + + Args: + query: The query string. + k: The number of results to return. + + Returns: + A list of matching Langchain Documents. + """ + logger.info(f"Performing similarity search for query: '{query}' with k={k}") + try: + results = self.vector_store.similarity_search(query, k=k) + logger.info(f"Found {len(results)} results.") + return results + except Exception as e: + logger.error(f"Error during similarity search: {e}", exc_info=True) + return [] diff --git a/ai/generative-ai-service/hr-goal-alignment/files/data_ingestion_courses.py b/ai/generative-ai-service/hr-goal-alignment/files/data_ingestion_courses.py new file mode 100644 index 000000000..fb6473100 --- /dev/null +++ b/ai/generative-ai-service/hr-goal-alignment/files/data_ingestion_courses.py @@ -0,0 +1,76 @@ +# Copyright (c) 2025 Oracle and/or its affiliates. +import logging +import sys +import oracledb +import config + +# Configure logging +logging.basicConfig( + level=logging.DEBUG, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + handlers=[ + logging.FileHandler("app.log"), # <- write logs into a file called app.log + logging.StreamHandler() # <- also allow printing to the console (terminal) + ] +) +logger = logging.getLogger(__name__) + +try: + print("Connecting to the database...") + oci_db_connection = oracledb.connect( + user=config.DB_USER, + password=config.DB_PASSWORD, + dsn=config.DB_DSN + ) + print("Connection successful.") + cursor = oci_db_connection.cursor() +except ImportError: + logging.error("Could not import get_db_connection from utils.py. Ensure it exists.") + sys.exit(1) + +from course_vector_utils import CourseVectorStore +import config # Project's config file + + + +def main(): + """ + Connects to the database, initializes the CourseVectorStore, + and populates it with data from the course catalog Excel file. + """ + logger.info("Starting course data ingestion...") + db_connection = None + try: + # Get database connection using the project's utility function + logger.info("Connecting to the database...") + db_connection = oci_db_connection + logger.info("Database connection successful.") + + # Initialize the vector store utility + logger.info("Initializing CourseVectorStore...") + vector_store_util = CourseVectorStore(db_connection) + + # Define the path to the Excel file + excel_file_path = "data/Full_Company_Training_Catalog.xlsx" + logger.info(f"Processing Excel file: {excel_file_path}") + + # Add courses from the Excel file to the vector store + vector_store_util.add_courses_from_excel(excel_file_path) + + logger.info("Course data ingestion completed successfully.") + + except Exception as e: + logger.error(f"An error occurred during data ingestion: {e}", exc_info=True) + sys.exit(1) # Exit with error code + + finally: + # Ensure the database connection is closed + if db_connection: + try: + db_connection.close() + logger.info("Database connection closed.") + except Exception as e: + logger.error(f"Error closing database connection: {e}", exc_info=True) + +if __name__ == "__main__": + main() diff --git a/ai/generative-ai-service/hr-goal-alignment/files/gen_ai_service/inference.py b/ai/generative-ai-service/hr-goal-alignment/files/gen_ai_service/inference.py new file mode 100644 index 000000000..aa007e471 --- /dev/null +++ b/ai/generative-ai-service/hr-goal-alignment/files/gen_ai_service/inference.py @@ -0,0 +1,193 @@ +# Copyright (c) 2025 Oracle and/or its affiliates. +import json +import logging +import sys +import os +from langchain_community.chat_models.oci_generative_ai import ChatOCIGenAI +from langchain_community.embeddings import OCIGenAIEmbeddings +from langchain_core.messages import HumanMessage, SystemMessage + +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) +import config + +# Set up logging +logging.basicConfig( + level=logging.DEBUG, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + handlers=[ + logging.FileHandler("app.log"), # <- write logs into a file called app.log + logging.StreamHandler() # <- also allow printing to the console (terminal) + ] +) +logger = logging.getLogger(__name__) + +def get_embedding_model(): + embedding_model = OCIGenAIEmbeddings( + model_id=config.EMBEDDING_MODEL, + service_endpoint=config.GEN_AI_ENDPOINT, + truncate="NONE", + compartment_id=config.OCI_COMPARTMENT_ID, + auth_type=config.AUTH_TYPE, + auth_profile=config.CONFIG_PROFILE, + ) + + return embedding_model + + +def get_llm_model(temperature: float = 0, max_tokens: int = 4000): + llm_model = ChatOCIGenAI( + auth_type=config.AUTH_TYPE, + model_id=config.LLM_MODEL, + service_endpoint=config.GEN_AI_ENDPOINT, + compartment_id=config.OCI_COMPARTMENT_ID, + provider=config.PROVIDER, + model_kwargs={ + "temperature": temperature, + "max_tokens": max_tokens, + }, + ) + return llm_model + + +def recommend_courses(profile, feedback) -> dict: + model = get_llm_model() + response = {} + try: + response = model.with_structured_output(schema).invoke( + [ + SystemMessage(content=prompt_sys), + HumanMessage( + content=prompt_user.format( + EMPLOYEE_PROFILE=profile, MANAGER_FEEDBACK=feedback + ) + ), + ] + ) + except Exception as e: + logger.error(f"Error during recommended_courses execution: {e}") + + return response # type: ignore + + + +def classify_smart_goal(goal_description) -> dict: + model = get_llm_model(temperature=0, max_tokens=600) + response = {} + try: + response = model.invoke( + [ + HumanMessage(content=goals_prompt_sys.format(GOAL_DESCRIPTION=goal_description, INSTRUCTIONS=instructions_prompt)), + ] + ) + except Exception as e: + logger.error(f"Error during recommended_courses execution: {e}") + + return json.loads(response.content) # type: ignore + + +prompt_sys = """ +# AI-powered Training Recommendation Engine +You are an AI-powered Training Recommendation Engine specialized in matching professional development courses to employee needs with strict prioritization rules. + +**Primary Objective:** +Analyze information to identify skill gaps and recommend relevant training courses according to this specific priority order: +1. Issues highlighted in manager feedback (highest priority) +2. Alignment with job title requirements and responsibilities +3. Complementing existing skills while addressing gaps +4. Appropriate for years of experience and current skill level + +**Output Requirements:** +1. Identify 3 clear focus areas derived primarily from the manager feedback +2. For each focus area, provide exactly 2 course recommendations that: + - Explicitly include difficulty level in the title: "(Beginner)", "(Intermediate)", or "(Advanced)" + - Are specific and descriptive of the skill being developed + - Match the employee's current experience level appropriately +3. All recommendations must be ordered by priority following the hierarchy above + +**Response Format:** +Return a clean, properly formatted JSON object with one keys: +- "recommended_courses": { + "Focus Area 1": [2 courses addressing this area], + "Focus Area 2": [2 courses addressing this area], + "Focus Area 3": [2 courses addressing this area] + } + +Example response (exactly as shown, without code formatting, backticks, or extra newlines): +{ + "recommended_courses": { + "Strategic Communication": [ + "Influential Communication for Technical Professionals (Intermediate)", + "Executive Presence and Presentation Skills (Advanced)" + ], + "Technical Leadership": [ + "Leading High-Performance Technical Teams (Intermediate)", + "Strategic Technical Decision Making (Advanced)" + ], + "Project Estimation": [ + "Fundamentals of Project Estimation (Beginner)", + "Advanced Techniques in Project Scoping and Estimation (Intermediate)" + ] + } +} + +DO NOT wrap your response in code blocks, backticks, or any other formatting. Return ONLY the raw JSON object itself. +""" + +prompt_user = """ +Based on the following information about an employee and their performance, recommend appropriate training courses: + +**Employee Profile:** +{EMPLOYEE_PROFILE} + +**Manager Feedback:** +{MANAGER_FEEDBACK} + +Carefully analyze both sources of information with this strict priority order: +1. Address the most critical issues in the manager's feedback FIRST +2. Then consider the employee's job title and core responsibilities +3. Then look at their existing skills and experience level +4. Finally, consider their years of experience + +Provide: +1. Three (3) specific focus areas this employee should develop (prioritized by importance) +2. For each focus area, recommend exactly 2 courses that address the specific skill gaps (total of 4-6 courses) + +Remember: +- Include the appropriate difficulty level in each course title: "(Beginner)", "(Intermediate)", or "(Advanced)" +- Order focus areas by importance with manager feedback concerns taking highest priority +- Match course difficulty to the employee's current experience level +- Format your response as a proper JSON object with "focus_areas" and "recommended_courses" keys, where "recommended_courses" is an object with focus areas as keys and arrays of 2 courses as values +- Return ONLY the raw JSON object itself with no markdown formatting, code blocks, or backticks +""" +goals_prompt_sys = """ +Determine if the following goal is SMART or not:\n +"{GOAL_DESCRIPTION}" +{INSTRUCTIONS} +""" +instructions_prompt = """ +Provide a structured response in this exact JSON format: +{ + "classification": "", + "details": { + "Specific": "", + "Measurable": "", + "Achievable": "", + "Relevant": "", + "TimeBound": "" + } +} +Return ONLY the JSON response without additional text, explanations, or formatting. +""" +schema = { + "title": "RecommendedCourses", + "type": "object", + "description": "A tool for retrieving recommended courses by skill area", + "properties": { + "recommended_courses": { + "type": "object", + "additionalProperties": {"type": "array", "items": {"type": "string"}}, + "description": "A mapping of skill areas to lists of recommended courses", + } + }, + "required": ["recommended_courses"], +} diff --git a/ai/generative-ai-service/hr-goal-alignment/files/goal_alignment_backend.py b/ai/generative-ai-service/hr-goal-alignment/files/goal_alignment_backend.py new file mode 100644 index 000000000..b8131b992 --- /dev/null +++ b/ai/generative-ai-service/hr-goal-alignment/files/goal_alignment_backend.py @@ -0,0 +1,525 @@ +# Copyright (c) 2025 Oracle and/or its affiliates. +import oracledb +import config +import json +from langchain.prompts import PromptTemplate +from langchain.schema import AIMessage +from langchain_community.chat_models.oci_generative_ai import ChatOCIGenAI +from langchain_core.runnables import RunnableSequence + +# Initialize Oracle LLM +llm = ChatOCIGenAI( + model_id=config.GENERATION_MODEL_ID, + service_endpoint=config.OCI_SERVICE_ENDPOINT, + compartment_id=config.OCI_COMPARTMENT_ID, + model_kwargs={"temperature": 0.1, "max_tokens": 500}, +) + +def query_llm(prompt_template, inputs) -> str: + """Formats prompt and calls the Oracle LLM""" + if not isinstance(prompt_template, PromptTemplate): + raise TypeError(" query_llm expected a PromptTemplate object.") + + for key, value in inputs.items(): + if isinstance(value, AIMessage): + inputs[key] = value.content + elif not isinstance(value, str): + inputs[key] = json.dumps(value, indent=2) + + chain = RunnableSequence(prompt_template | llm) + response = chain.invoke(inputs) + + return response.content if isinstance(response, AIMessage) else str(response) # type: ignore + +# --- Database Data Loading Functions --- + +def load_self_assessment_from_db(connection, employee_id): + """Loads self assessment data from the Self_Assessments table.""" + assessment_data = {} + try: + cursor = connection.cursor() + cursor.execute( + """ + SELECT assessment_id, key_achievements, skill_development, strengths, + areas_for_improvement, alignment_with_career_goals + FROM Self_Assessments + WHERE employee_id = :1 + """, + (employee_id,) + ) + result = cursor.fetchone() + if result: + # Read CLOBs properly + assessment_id, key_achievements_lob, skill_dev_lob, strengths_lob, areas_lob, alignment_lob = result + assessment_data = { + "assessment_id": assessment_id, + "key_achievements": key_achievements_lob.read() if key_achievements_lob else None, + "skill_development": skill_dev_lob.read() if skill_dev_lob else None, + "strengths": strengths_lob.read() if strengths_lob else None, + "areas_for_improvement": areas_lob.read() if areas_lob else None, + "alignment_with_career_goals": alignment_lob.read() if alignment_lob else None, + } + cursor.close() # Close cursor after use + except oracledb.Error as error: + print(f"Error loading self assessment for {employee_id}: {error}") + # Return empty dict or raise error depending on desired handling + return assessment_data + +def load_manager_briefing_from_db(connection, employee_id): + """Loads manager briefing data from the Manager_Briefings table.""" + briefing_data = {} + try: + cursor = connection.cursor() + cursor.execute( + """ + SELECT briefing_id, quantitative_performance, perception_gap, manager_context, + tenure, cross_departmental_contributions, career_alignment_prompt + FROM Manager_Briefings + WHERE employee_id = :1 + """, + (employee_id,) + ) + result = cursor.fetchone() + if result: + # Read CLOBs properly + briefing_id, quant_perf_lob, percep_gap_lob, mgr_context_lob, tenure_lob, cross_dept_lob, career_prompt_lob = result + briefing_data = { + "briefing_id": briefing_id, + "quantitative_performance": quant_perf_lob.read() if quant_perf_lob else None, + "perception_gap": percep_gap_lob.read() if percep_gap_lob else None, + "manager_context": mgr_context_lob.read() if mgr_context_lob else None, + "tenure": tenure_lob.read() if tenure_lob else None, + "cross_departmental_contributions": cross_dept_lob.read() if cross_dept_lob else None, + "career_alignment_prompt": career_prompt_lob.read() if career_prompt_lob else None, + } + cursor.close() # Close cursor after use + except oracledb.Error as error: + print(f"Error loading manager briefing for {employee_id}: {error}") + # Return empty dict or raise error + return briefing_data + +# πŸš€ Goal Alignment Functions (Database Version) + +def get_horizontal_peers(connection, employee_id): + try: + cursor = connection.cursor() + cursor.execute( + """ + SELECT e2.name + FROM Employees e1 + JOIN Employees e2 ON e1.manager_id = e2.manager_id + WHERE e1.employee_id = :1 AND e2.employee_id != :1 + """, + (employee_id, employee_id) # πŸ‘ˆ Pass it twice + ) + peers = [row[0] for row in cursor.fetchall()] + return peers + except oracledb.Error as error: + print("Error fetching horizontal peers:") + print(error) + return [] + + +def vertical_downward_employees(connection, manager_id): + """Finds employees who report directly to a given manager.""" + try: + cursor = connection.cursor() + cursor.execute( + """ + SELECT name + FROM Employees + WHERE manager_id = :1 + """, + (manager_id,) + ) + reports = [row[0] for row in cursor.fetchall()] + return reports + except oracledb.Error as error: + print("Error fetching vertical downward employees:") + print(error) + return [] + +def check_vertical_alignment_upward(connection, manager_id, employee_id): + """Checks vertical alignment between an employee and their manager.""" + try: + cursor = connection.cursor() + + # Fetch manager and employee data + cursor.execute( + """ + SELECT e1.name, e1.role, e2.name, e2.role + FROM Employees e1 + JOIN Employees e2 ON e1.employee_id = e2.manager_id + WHERE e1.employee_id = :1 AND e2.employee_id = :2 + """, + (manager_id, employee_id) + ) + result = cursor.fetchone() + + if not result: + return f"No record found for employee_id {employee_id} or manager_id {manager_id}." + + manager_name, manager_role, employee_name, employee_role = result + + # Fetch manager goals + cursor.execute( + """ + SELECT title, objective, metrics, timeline + FROM Goals + WHERE employee_id = :1 + """, + (manager_id,) + ) + manager_goals = [ + tuple(col.read() if isinstance(col, oracledb.LOB) else col for col in row) + for row in cursor.fetchall() + ] + + # Fetch employee goals + cursor.execute( + """ + SELECT title, objective, metrics, timeline + FROM Goals + WHERE employee_id = :1 + """, + (employee_id,) + ) + employee_goals = [ + tuple(col.read() if isinstance(col, oracledb.LOB) else col for col in row) + for row in cursor.fetchall() + ] + + prompt_template = PromptTemplate( + input_variables=["manager_name", "employee_name", "manager_goals", "employee_goals"], + template=""" +### Reference Data (Use Only This Information) +**Manager:** {manager_name} +**Employee:** {employee_name} + +Manager Goals: {manager_goals} +Employee Goals: {employee_goals} + +Compare the goals: +- Does the employee’s goal structure align with the manager’s? +- Are the success metrics matching? If not, suggest refinements. +- Highlight any missing objectives. + """ + ) + + return query_llm(prompt_template, { + "manager_name": manager_name, + "employee_name": employee_name, + "manager_goals": manager_goals, + "employee_goals": employee_goals + }) + + except oracledb.Error as error: + print("Error checking vertical alignment:") + print(error) + return "Error checking vertical alignment." + +def check_if_vertical_aligned(connection, manager_id, employee_id): + """Checks vertical alignment between an employee and their manager.""" + try: + cursor = connection.cursor() + + # Fetch manager and employee data + cursor.execute( + """ + SELECT e1.name, e1.role, e2.name, e2.role + FROM Employees e1 + JOIN Employees e2 ON e1.employee_id = e2.manager_id + WHERE e1.employee_id = :1 AND e2.employee_id = :2 + """, + (manager_id, employee_id) + ) + result = cursor.fetchone() + + if not result: + return f"No record found for employee_id {employee_id} or manager_id {manager_id}." + + manager_name, manager_role, employee_name, employee_role = result + + # Fetch manager goals + cursor.execute( + """ + SELECT title, objective, metrics, timeline + FROM Goals + WHERE employee_id = :1 + """, + (manager_id,) + ) + manager_goals = [ + tuple(col.read() if isinstance(col, oracledb.LOB) else col for col in row) + for row in cursor.fetchall() + ] + + # Fetch employee goals + cursor.execute( + """ + SELECT title, objective, metrics, timeline + FROM Goals + WHERE employee_id = :1 + """, + (employee_id,) + ) + employee_goals = [ + tuple(col.read() if isinstance(col, oracledb.LOB) else col for col in row) + for row in cursor.fetchall() + ] + + prompt_template = PromptTemplate( + input_variables=["manager_name", "employee_name", "manager_goals", "employee_goals"], + template=""" + ### Vertical Alignment Assessment + + INSTRUCTIONS: Evaluate the vertical alignment score for **Employee {employee_name}** relative to **Manager {manager_name}** using SOLELY the reference data below. Return ONLY a single integer from **0 to 100** representing the overall vertical alignment. + + REFERENCE DATA: + - Manager: {manager_name} + - Manager Goals: {manager_goals} + - Employee: {employee_name} + - Employee Goals: {employee_goals} + + ASSESSMENT CRITERIA + 1. GOAL STRUCTURE ALIGNMENT (40%) – How closely the employee’s goal framework mirrors the manager’s stated goals. + - **0** : No structural overlap + - **20** : Partial alignment on some themes + - **40** : Full structural correspondence on all key themes + + 2. SUCCESS-METRIC CONSISTENCY (30%) – Degree to which the employee’s KPIs/OKRs use the same or compatible metrics as the manager’s. + - **0** : Metrics unrelated or absent + - **15** : Metrics partially match or are loosely comparable + - **30** : Metrics directly map to the manager’s success measures + + 3. OBJECTIVE COMPLETENESS (30%) – Coverage of all critical objectives set by the manager, plus identification of any missing elements. + - **0** : Several critical objectives missing + - **15** : Some objectives missing or vaguely addressed + - **30** : All manager objectives addressed and gaps proactively filled + + OUTPUT FORMAT + Return **ONLY** a single integer between 0 and 100 with no additional text, punctuation, or formatting. + + EXAMPLE RESPONSE + 87 + """ + ) + + return query_llm(prompt_template, { + "manager_name": manager_name, + "employee_name": employee_name, + "manager_goals": manager_goals, + "employee_goals": employee_goals + }) + + except oracledb.Error as error: + print("Error checking vertical alignment:") + print(error) + return "Error checking vertical alignment." + +def check_horizontal_alignment(connection, employee_id): + """Checks cross-team alignment using data from the database.""" + try: + cursor = connection.cursor() + + # Get the list of peers who share the same manager + list_horizontal_peers = get_horizontal_peers(connection, employee_id) + + # Fetch employee goals + cursor.execute( + """ + SELECT title, objective, metrics, timeline + FROM Goals + WHERE employee_id = :1 + """, + (employee_id,) + ) + employee_goals = [ + tuple(col.read() if isinstance(col, oracledb.LOB) else col for col in row) + for row in cursor.fetchall() + ] + + + if not employee_goals: + return f"⚠️ No goal data is available for horizontal alignment." + + prompt_template = PromptTemplate( + input_variables=["employee_id", "employee_goals", "list_horizontal_peers"], + template=""" + ### Reference Data (Use Only This Information) + Evaluate the cross-functional alignment of goals for employee ID {employee_id}. + Restrict your comparison to the following peers: {list_horizontal_peers}. + + - **Employee ID:** {employee_id} + - **Goals:** {employee_goals} + + 1. Are there dependencies on other departments? + 2. Are any key objectives missing that would improve alignment? + 3. Suggest ways to ensure better collaboration. + """ + ) + + return query_llm(prompt_template, { + "employee_id": employee_id, + "employee_goals": employee_goals, + "list_horizontal_peers": list_horizontal_peers + }) + + except oracledb.Error as error: + print("Error checking horizontal alignment:") + print(error) + return "Error checking horizontal alignment." + +def check_if_horizontal_aligned(connection, employee_id): + try: + cursor = connection.cursor() + + # Get the list of peers who share the same manager + list_horizontal_peers = get_horizontal_peers(connection, employee_id) + + # Fetch employee goals + cursor.execute( + """ + SELECT title, objective, metrics, timeline + FROM Goals + WHERE employee_id = :1 + """, + (employee_id,) + ) + employee_goals = [ + tuple(col.read() if isinstance(col, oracledb.LOB) else col for col in row) + for row in cursor.fetchall() + ] + + + if not employee_goals: + return f"⚠️ No goal data is available for horizontal alignment." + + prompt_template = PromptTemplate( + input_variables=["employee_id", "employee_goals", "list_horizontal_peers"], + template=""" + ### Horizontal Alignment Assessment + + INSTRUCTIONS: Analyze the cross-functional alignment score for employee ID {employee_id} based SOLELY on the provided data. Return ONLY a single numerical value from 0-100 representing the degree of horizontal alignment. + + REFERENCE DATA: + - Employee ID: {employee_id} + - Employee Goals: {employee_goals} + - Relevant Horizontal Peers: {list_horizontal_peers} + + ASSESSMENT CRITERIA: + 1. DEPENDENCY ANALYSIS (40%): Measure how effectively the employee's goals account for dependencies with peer departments. + - 0: No acknowledgment of cross-functional dependencies + - 25: Basic recognition of dependencies + - 40: Comprehensive integration of all relevant dependencies + + 2. COMPLETENESS ASSESSMENT (30%): Evaluate if the goals include all necessary objectives for optimal horizontal alignment. + - 0: Critical alignment objectives missing + - 15: Some alignment objectives present + - 30: All essential alignment objectives included + + 3. COLLABORATION POTENTIAL (30%): Assess how well the goals facilitate collaborative execution. + - 0: Goals create siloed work patterns + - 15: Goals permit basic collaboration + - 30: Goals actively enable optimal collaboration + + OUTPUT FORMAT: + Return ONLY a single integer between 0-100 representing the total horizontal alignment score without additional text, explanations, or formatting. + + EXAMPLE RESPONSE: + 78 + """ + ) + + return query_llm(prompt_template, { + "employee_id": employee_id, + "employee_goals": employee_goals, + "list_horizontal_peers": list_horizontal_peers + }) + + except oracledb.Error as error: + print("Error checking horizontal alignment:") + print(error) + return "Error checking horizontal alignment." + +def generate_final_recommendations(connection, employee_id): + """Creates a final summary of recommendations.""" + try: + cursor = connection.cursor() + + # Get the manager ID + cursor.execute( + """ + SELECT manager_id + FROM Employees + WHERE employee_id = :1 + """, + (employee_id,) + ) + result = cursor.fetchone() + if not result: + return f"No manager found for employee ID {employee_id}." + + manager_id = result[0] + + # Get alignment results (already LLM-formatted strings) + vertical = check_vertical_alignment_upward(connection, manager_id, employee_id) + horizontal_peers = get_horizontal_peers(connection, employee_id) + + # Ensure horizontal is a string (not a list) + horizontal_summary = ", ".join(horizontal_peers) if horizontal_peers else "No peers found." + + prompt_template = PromptTemplate( + input_variables=["vertical", "horizontal", "employee_id"], + template=""" +**Final Recommendations for Employee ID {employee_id}:** + +- **Vertical Alignment:** {vertical} +- **Horizontal Collaboration Opportunities:** {horizontal} + +Provide a structured, clear, and actionable summary. + """ + ) + + response = query_llm(prompt_template, { + "vertical": vertical, + "horizontal": horizontal_summary, + "employee_id": employee_id + }) + + # Ensure final output is a clean string (no LOBs sneaking in) + return str(response) + + except oracledb.Error as error: + print("Error generating final recommendations:") + print(error) + return "Error generating final recommendations." + +def update_employee_goal_objective(connection, employee_id, new_objective): + """Updates the 'objective' field for a given employee in the Goals table.""" + try: + cursor = connection.cursor() + + # Optionally fetch to check if a goal exists + cursor.execute( + "SELECT COUNT(*) FROM Goals WHERE employee_id = :1", + (employee_id,) + ) + if cursor.fetchone()[0] == 0: + return f"No goal entry found for employee ID {employee_id}." + + cursor.execute( + """ + UPDATE Goals + SET objective = :1 + WHERE employee_id = :2 + """, + (new_objective, employee_id) + ) + connection.commit() + return "Goal objective updated successfully." + + except oracledb.Error as error: + print("Error updating goal objective:") + print(error) + return "Error updating goal objective." diff --git a/ai/generative-ai-service/hr-goal-alignment/files/org_chart_backend.py b/ai/generative-ai-service/hr-goal-alignment/files/org_chart_backend.py new file mode 100644 index 000000000..8a8e2d558 --- /dev/null +++ b/ai/generative-ai-service/hr-goal-alignment/files/org_chart_backend.py @@ -0,0 +1,111 @@ +# Copyright (c) 2025 Oracle and/or its affiliates. +import oracledb +import pandas as pd + +import config +from gen_ai_service.inference import classify_smart_goal +from goal_alignment_backend import check_if_horizontal_aligned, check_if_vertical_aligned + +connection = oracledb.connect( + **config.CONNECT_ARGS_VECTOR + ) +def mapping_all_employees() -> tuple[dict[str, list[str]], pd.DataFrame]: + query = ( + "SELECT employee_id, name, role, manager_id " + "FROM Employees" + ) + df_db = pd.DataFrame() # Initialize df_db + try: + df_db = pd.read_sql(query, connection) # type: ignore + + except Exception as err: + print(f"Query failed: {err}") + connection.close() + + # connection.close() + return mapping_from_df(df_db), df_db + + +def build_label(df_row: pd.Series) -> str: + """Helper – build multiline label *Name (Title)* for a row.""" + return ( + f"{df_row['NAME'].strip()}\n(" + f"{df_row['ROLE'].strip()})" + ) + + +def mapping_from_df(df: pd.DataFrame) -> dict[str, list[str]]: + """Convert DataFrame into {manager_label: [direct_report_labels]} mapping.""" + df = df.copy() + df["label"] = df.apply(build_label, axis=1) + id_to_label: dict[int, str] = dict(zip(df["EMPLOYEE_ID"], df["label"])) + mapping: dict[str, list[str]] = {} + for _, row in df.iterrows(): + mgr_id = row.get("MANAGER_ID") + if pd.notna(mgr_id): + parent = id_to_label.get(mgr_id) + child = id_to_label.get(row["EMPLOYEE_ID"]) + if parent and child: + mapping.setdefault(parent, []).append(child) + return mapping + +def check_smart_goal(df_row: pd.Series) -> str: + print(df_row.to_dict()) + goal_smart = classify_smart_goal(df_row.to_dict()) + return goal_smart.get("classification", "N/A") + + +def fetch_goals_from_emp(df, emp_data) -> pd.DataFrame: + df_db = pd.DataFrame() # Initialize as empty DataFrame + try: + emp_id = search_employee(df, emp_data) + if emp_id: # Only proceed if emp_id is found + query = f"SELECT title, objective, metrics, timeline FROM Goals WHERE employee_id = '{emp_id}'" + df_db = pd.read_sql(query, connection) # type: ignore + + except oracledb.Error as err: + print(f"Oracle connection error: {err}") + # df_db remains an empty DataFrame if an error occurs + + if not df_db.empty: # Check if DataFrame is not empty before applying + df_db["smart"] = df_db.apply(check_smart_goal, axis=1) + + return df_db + + +def search_employee(df, param): + parts = param.split('\n') + + if len(parts) < 2: + return None # Invalid input format + + full_name = parts[0] + job_title = parts[1].replace('(', '').replace(')', '') # Remove parentheses from job title + + filtered_df = df[(df['NAME'] == full_name) & + (df['ROLE'] == job_title)] + + if not filtered_df.empty: + return filtered_df['EMPLOYEE_ID'].iloc[0] + else: + return None + + +def check_goal_alignment(df, emp_data, manager_data): + vertical = None + print(manager_data) + emp_id = search_employee(df, emp_data) + if manager_data: + manager_id = search_employee(df, manager_data) + vertical = check_if_vertical_aligned(connection, manager_id, emp_id) + horizontal = check_if_horizontal_aligned(connection, emp_id) + if isinstance(horizontal, str) and horizontal.isdigit(): + horizontal = int(horizontal) + else: + horizontal = None + if isinstance(vertical, str) and vertical.isdigit(): + vertical = int(vertical) + else: + vertical = None + + return vertical, horizontal diff --git a/ai/generative-ai-service/hr-goal-alignment/files/pages/course_recommendation_chatbot.py b/ai/generative-ai-service/hr-goal-alignment/files/pages/course_recommendation_chatbot.py new file mode 100644 index 000000000..2ddc46567 --- /dev/null +++ b/ai/generative-ai-service/hr-goal-alignment/files/pages/course_recommendation_chatbot.py @@ -0,0 +1,148 @@ +# Copyright (c) 2025 Oracle and/or its affiliates. +import streamlit as st +import logging +import oracledb +import config + +# Import necessary components from the current project +from course_vector_utils import CourseVectorStore +from gen_ai_service import inference as gen_ai + +# Configure logging +logging.basicConfig( + level=logging.DEBUG, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + handlers=[ + logging.FileHandler("app.log"), + logging.StreamHandler() + ] +) +logger = logging.getLogger(__name__) + +ICONS = { + 'beginner': 'πŸ₯‰ Beginner', + 'intermediate': 'πŸ₯ˆ Intermediate', + 'advanced': 'πŸ₯‡ Advanced', + 'default': '🎯 General' +} + +def display_course(course): + """Display a single course recommendation block in Streamlit.""" + name = course.get('NAME', 'No Name Provided') + url = course.get('URL', '#') + rating = course.get('RATING', 'N/A') + level_key = course.get('DIFFICULTYLEVEL', 'default').strip().lower() + icon = ICONS.get(level_key, ICONS['default']) + university = course.get('UNIVERSITY', 'N/A') + description = course.get('DESCRIPTION', 'No description provided.') + skills = course.get('SKILLS', 'N/A') + + st.markdown(f"**[{name}]({url})** (⭐ {rating})") + st.markdown(f"> {icon} | πŸ›οΈ {university}") + st.markdown(f"> _{description[:150]}{'...' if len(description) > 150 else ''}_") + st.markdown(f"> **Skills:** {skills}\n\n") + +@st.cache_resource +def initialize_resources(): + db_conn = None + try: + logger.info("Initializing database connection...") + db_conn = oracledb.connect( + user=config.DB_USER, + password=config.DB_PASSWORD, + dsn=config.DB_DSN + ) + logger.info("Database connection successful.") + + logger.info("Initializing vector store...") + vector_store = CourseVectorStore(db_conn) + logger.info("Vector store initialized successfully.") + return vector_store + + except oracledb.Error as db_err: + logger.error(f"Oracle Database connection error: {db_err}", exc_info=True) + st.error("Failed to connect to the database. Please check credentials and network.") + if db_conn: + db_conn.close() + return None + except Exception as e: + logger.error(f"Failed to initialize resources: {e}", exc_info=True) + st.error("Failed to initialize resources. Please check logs or contact support.") + if db_conn: + db_conn.close() + return None + +vector_store = initialize_resources() + +st.title("πŸŽ“ Course Recommendation Chatbot") + +st.write(""" +Enter your learning goals, desired skills, or topics you're interested in. +The chatbot will search the course catalog and recommend relevant training. +""") + +user_input = st.text_area("Describe your learning needs:", height=100) + +if st.button("πŸ” Find Courses") and vector_store: + if not user_input: + st.warning("Please enter your learning needs in the text area.") + else: + with st.spinner("Searching for relevant courses and generating recommendations..."): + try: + logger.info(f"Performing vector search for query: '{user_input}'") + retrieved_docs = vector_store.similarity_search(user_input, k=5) + + if not retrieved_docs: + st.info("No directly matching courses found in the vector store based on your query.") + else: + context = "\n\n".join([ + f"Course: {doc.metadata.get('NAME', 'N/A')}\nDescription: {doc.metadata.get('DESCRIPTION', 'N/A')}\nSkills: {doc.metadata.get('SKILLS', 'N/A')}" + for doc in retrieved_docs + ]) + logger.info(f"Context prepared for LLM:\n{context[:500]}...") + + logger.info("Calling GenAI service for recommendations...") + recommendations = gen_ai.recommend_courses(user_input, context) + logger.info(f"Received recommendations from GenAI: {recommendations}") + + if recommendations and isinstance(recommendations, list): + st.markdown("### πŸ’‘ Recommended Courses") + for item in recommendations: + display_course(item) + + elif isinstance(recommendations, dict) and recommendations.get("recommended_courses"): + st.markdown("### πŸ’‘ Recommended Courses") + focus_areas_data = recommendations["recommended_courses"] + if not focus_areas_data: + st.info("The AI could not determine specific recommendations based on the search results.") + else: + courses_list = [] + if isinstance(focus_areas_data, list): + courses_list = focus_areas_data + elif isinstance(focus_areas_data, dict): + st.markdown("### πŸ’‘ Recommended Courses by Focus Area") + for focus_area, courses in focus_areas_data.items(): + st.subheader(f"πŸ“Œ {focus_area}") + if not isinstance(courses, list): + st.warning(f"Unexpected structure under '{focus_area}' β€” skipping.") + continue + for item in courses: + if isinstance(item, dict): + display_course(item) + elif isinstance(item, str): + st.markdown(f"- πŸ“š {item}") + else: + st.warning("Unrecognized course format.") + if courses_list: + for item in courses_list: + display_course(item) + + else: + st.info("The AI assistant processed the search results but didn't provide specific recommendations in the expected format.") + logger.warning(f"Unexpected recommendation format: {recommendations}") + except Exception as e: + logger.error(f"An error occurred while recommending courses: {e}", exc_info=True) + st.error("Something went wrong while processing your request. Please try again.") + +st.markdown("---") +st.info("Powered by Oracle Cloud Infrastructure Generative AI") diff --git a/ai/generative-ai-service/hr-goal-alignment/files/pages/goal_alignment_chatbot.py b/ai/generative-ai-service/hr-goal-alignment/files/pages/goal_alignment_chatbot.py new file mode 100644 index 000000000..122669bd6 --- /dev/null +++ b/ai/generative-ai-service/hr-goal-alignment/files/pages/goal_alignment_chatbot.py @@ -0,0 +1,230 @@ +# Copyright (c) 2025 Oracle and/or its affiliates. +import streamlit as st +import json +import oracledb +import config +import re +from datetime import datetime +from utils import get_employee_id_by_name, generate_transcript_docx +from langchain.prompts import PromptTemplate +from goal_alignment_backend import ( + check_vertical_alignment_upward, + check_horizontal_alignment, + generate_final_recommendations, + query_llm, + update_employee_goal_objective +) + +# --- Helpers --- +def is_refinement_suggestion(text: str) -> bool: + keywords = [ + "i suggest refining", "consider adding a goal", "consider including", + "you could strengthen alignment by", "i recommend creating", "you could add", + "i suggest you add", "you could include" + ] + return any(kw in text.lower() for kw in keywords) + +def extract_possible_goal(chat_message: str) -> str: + match = re.search(r'your goal,.*?\"(.*?)\"', chat_message) + return match.group(1) if match else "Refined Goal" + +def add_goal_refinement(goal_title, refined_objective): + existing = next((g for g in st.session_state.ga_goal_refinements if g["goal_title"] == goal_title), None) + if not existing: + st.session_state.ga_goal_refinements.append({ + "goal_title": goal_title, + "refined_objective": refined_objective, + "timestamp": datetime.utcnow().isoformat(), + "applied": False + }) + +def render_refinement_action(refinement, idx): + st.markdown(f"**Goal:** {refinement['goal_title']}") + st.text_area("Refined Objective", value=refinement["refined_objective"], height=120, key=f"refined_{idx}", disabled=True) + if not refinement["applied"]: + if st.button(f"βœ… Apply to DB (Goal: {refinement['goal_title']})", key=f"apply_{idx}"): + connection = None + try: + connection = oracledb.connect( + user=config.DB_USER, + password=config.DB_PASSWORD, + dsn=config.DB_DSN + ) + employee_id = get_employee_id_by_name(connection, st.session_state.ga_employee_name) + msg = update_employee_goal_objective(connection, employee_id, refinement["refined_objective"]) + refinement["applied"] = True + st.success(f"Updated: {msg}") + except Exception as e: + st.error(f"Error updating DB: {e}") + finally: + if connection: + connection.close() + +# --- Main Chatbot --- +def run_goal_alignment_chatbot(): + st.title("πŸ“Š Goal Alignment Chatbot") + + st.session_state.setdefault("ga_goal_refinements", []) + st.session_state.setdefault("ga_chat_history", []) + st.session_state.setdefault("ga_conversation_complete", False) + + if "ga_report" not in st.session_state: + employee_name = st.text_input("Enter Employee Name:", key="ga_employee_input") + + if st.button("Generate Report", key="ga_generate_report_btn"): + with st.spinner("Connecting and compiling alignment report..."): + connection = None + try: + connection = oracledb.connect( + user=config.DB_USER, + password=config.DB_PASSWORD, + dsn=config.DB_DSN + ) + employee_id = get_employee_id_by_name(connection, employee_name) + + if not employee_id: + st.error(f"Employee '{employee_name}' not found in the database.") + return + + cursor = connection.cursor() + cursor.execute("SELECT manager_id FROM Employees WHERE employee_id = :1", (employee_id,)) + result = cursor.fetchone() + if not result: + st.error(f"No manager found for employee '{employee_name}'") + return + manager_id = result[0] + + compiled_report = { + "vertical_alignment": check_vertical_alignment_upward(connection, manager_id, employee_id), + "horizontal_alignment": check_horizontal_alignment(connection, employee_id), + "final_recommendations": generate_final_recommendations(connection, employee_id) + } + + st.session_state.ga_report = compiled_report + st.session_state.ga_employee_name = employee_name + st.success(f"Report compiled for {employee_name}!") + + report_json = json.dumps(compiled_report, indent=2) + st.download_button( + label="πŸ“„ Download JSON Report", + data=report_json, + file_name=f"goal_alignment_report_{employee_name}.json", + mime="application/json" + ) + + initial_prompt = PromptTemplate( + input_variables=["employee_name", "final_recommendations"], + template=""" +You are a career mentor guiding {employee_name}. + +**Initial Guidance:** +{final_recommendations} + +Start the conversation: +- Give a friendly greeting in less than 100 words. +- Highlight one key insight from the report about how the employee aligns (or not) with their manager in less than 100 words. +- Suggest a concrete measure for {employee_name} to fix this, making reference to the text they have now in their own goal sheet and how it differs from that of their manager's. +- Only use the information in {final_recommendations}. + """ + ) + initial_message = query_llm(initial_prompt, { + "employee_name": employee_name, + "final_recommendations": compiled_report["final_recommendations"] + }) + st.session_state.ga_chat_history = [{"Chatbot": initial_message}] + + except Exception as e: + st.error(f"Error: {e}") + finally: + if connection: + connection.close() + + # --- Chat Interface --- + if "ga_report" in st.session_state: + st.subheader("Chat with your Career Mentor") + employee_name = st.session_state.ga_employee_name + + for exchange in st.session_state.ga_chat_history: + if "User" in exchange: + with st.chat_message("user", avatar="πŸ§‘β€πŸ’Ό"): + st.markdown(exchange["User"]) + if "Chatbot" in exchange: + with st.chat_message("assistant", avatar="πŸ€–"): + st.markdown(exchange["Chatbot"]) + + if not st.session_state.ga_conversation_complete: + user_msg = st.chat_input("You:", key="ga_user_input") + if user_msg: + st.session_state.ga_chat_history.append({"User": user_msg}) + + if any(phrase in user_msg.strip().lower() for phrase in [ + "no thanks", "no thank you", "i'm good", "im good", + "no i'm good", "no im good", "that's all", "we're done", + "we are done", "stop", "all set"]): + closing = "I hope you're feeling more confident in refining your goal sheet. Best of luck as you continue developing your plan!" + st.session_state.ga_chat_history.append({"Chatbot": closing}) + st.session_state.ga_conversation_complete = True + st.rerun() + + prompt = PromptTemplate( + input_variables=["employee_name", "chat_input", "chat_history", "full_report"], + template=""" +You are a helpful career mentor chatbot coaching {employee_name} on goal alignment. + +Here is the previous chat history: +{chat_history} + +Employee's latest message: +{chat_input} + +Report Findings: +{full_report} + +Instructions: +- Address one topic at a time from the Report Findings: vertical, horizontal, and recommendations. +- If the conversation history has had more than 2 messages on a single topic, move on to the next topic in the report. +- If the current topic was already discussed and the user gave any positive or acknowledging response (even brief ones like "ok", "will do", "makes sense"), move on to the next topic. +- Avoid restating earlier suggestions unless the user expressed confusion or disagreement. +- If there's a lack of agency in what they last said, suggest something to help them refine this goal but you're moving on regardless! +- Don’t repeat topics already covered in chat history. +- Don't ask open ended questions unless there's really no other option. +- - Give the impression that you're reducing the user's cognitive load, not adding to it. + """ + ) + response = query_llm(prompt, { + "employee_name": employee_name, + "chat_input": user_msg, + "chat_history": json.dumps(st.session_state.ga_chat_history, indent=2), + "full_report": json.dumps(st.session_state.ga_report, indent=2) + }) + + st.session_state.ga_chat_history.append({"Chatbot": response}) + if is_refinement_suggestion(response): + possible_goal = extract_possible_goal(response) + add_goal_refinement(possible_goal, response) + st.rerun() + + if st.button("Stop Conversation") and not st.session_state.ga_conversation_complete: + st.session_state.ga_chat_history.append({ + "Chatbot": "I hope you're feeling more confident in refining your goal sheet. Best of luck as you continue developing your plan!" + }) + st.session_state.ga_conversation_complete = True + st.rerun() + + if st.session_state.ga_conversation_complete and st.session_state.ga_chat_history: + transcript_file = generate_transcript_docx(st.session_state.ga_chat_history, employee_name) + st.download_button( + label="πŸ“₯ Download Conversation Transcript", + data=transcript_file, + file_name=f"{employee_name}_GoalAlignmentChat_transcript.docx", + mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document", + key="ga_download_btn" + ) + + if st.session_state.ga_conversation_complete and st.session_state.ga_goal_refinements: + st.markdown("### ✍️ Apply Refined Goals to the Database") + st.info("Below are refined objectives suggested during your chat. You can apply them to the goal sheet.") + for idx, refinement in enumerate(st.session_state.ga_goal_refinements): + render_refinement_action(refinement, idx) + +run_goal_alignment_chatbot() diff --git a/ai/generative-ai-service/hr-goal-alignment/files/pages/manager_meeting_chatbot.py b/ai/generative-ai-service/hr-goal-alignment/files/pages/manager_meeting_chatbot.py new file mode 100644 index 000000000..97ba6f14c --- /dev/null +++ b/ai/generative-ai-service/hr-goal-alignment/files/pages/manager_meeting_chatbot.py @@ -0,0 +1,137 @@ +# Copyright (c) 2025 Oracle and/or its affiliates. +import streamlit as st +import oracledb +import config +from langchain.prompts import PromptTemplate +from utils import get_employee_id_by_name +from goal_alignment_backend import query_llm + +def run_meeting_preparation_chatbot(): + st.title("Manager Meeting Preparation Chatbot") + + employee_name = st.text_input("Enter Employee Name:", key="prep_employee_input") + + if employee_name: + connection = None + cursor = None + try: + with st.spinner("Connecting and fetching data..."): + connection = oracledb.connect( + user=config.DB_USER, + password=config.DB_PASSWORD, + dsn=config.DB_DSN + ) + cursor = connection.cursor() + + employee_id = get_employee_id_by_name(connection, employee_name) + + if not employee_id: + st.error(f"Employee '{employee_name}' not found.") + return + + cursor.execute( + """ + SELECT sa.key_achievements, sa.skill_development, sa.strengths, sa.areas_for_improvement, sa.alignment_with_career_goals, + mb.quantitative_performance, mb.perception_gap, mb.manager_context, mb.tenure, mb.cross_departmental_contributions, mb.career_alignment_prompt + FROM Self_Assessments sa + LEFT JOIN Manager_Briefings mb ON sa.employee_id = mb.employee_id + WHERE sa.employee_id = :1 + """, + (employee_id,) + ) + + result = cursor.fetchone() + if not result: + st.warning(f"No data found for employee ID: {employee_id}") + return + + # Handle LOBs + def read_lob(lob): return lob.read() if lob else "N/A" + ( + key_achievements, skill_dev, strengths, areas_improvement, alignment_goals, + quant_perf, perception_gap, manager_context, tenure, cross_dept, career_prompt + ) = [read_lob(lob) for lob in result] + + st.subheader("Briefing Overview") + st.write(f"**Key Achievements:** {key_achievements}") + st.write(f"**Skill Development:** {skill_dev}") + st.write(f"**Strengths:** {strengths}") + st.write(f"**Areas for Improvement:** {areas_improvement}") + st.write(f"**Alignment with Career Goals:** {alignment_goals}") + st.write(f"**Quantitative Performance:** {quant_perf}") + st.write(f"**Perception Gap:** {perception_gap}") + st.write(f"**Manager Context:** {manager_context}") + st.write(f"**Tenure:** {tenure}") + st.write(f"**Cross-Departmental Contributions:** {cross_dept}") + st.write(f"**Career Alignment Prompt:** {career_prompt}") + + st.subheader("Chat with Meeting Prep Bot") + st.write("Ask questions or discuss points to prepare for your meeting.") + + if "prep_messages" not in st.session_state: + st.session_state.prep_messages = [] + + for message in st.session_state.prep_messages: + with st.chat_message(message["role"]): + st.markdown(message["content"]) + + if user_input := st.chat_input("What do you want to discuss?", key="prep_chat_input"): + st.session_state.prep_messages.append({"role": "user", "content": user_input}) + with st.chat_message("user"): + st.markdown(user_input) + + context = { + "employee_name": employee_name, + "user_input": user_input, + "key_achievements": key_achievements, + "skill_development": skill_dev, + "strengths": strengths, + "areas_for_improvement": areas_improvement, + "alignment_with_career_goals": alignment_goals, + "quantitative_performance": quant_perf, + "perception_gap": perception_gap, + "manager_context": manager_context, + "tenure": tenure, + "cross_departmental_contributions": cross_dept, + "career_alignment_prompt": career_prompt + } + + prompt = PromptTemplate( + input_variables=list(context.keys()), + template=""" + You are a helpful HR assistant chatbot. + You are helping {employee_name} prepare for their meeting with their manager based on their self-assessment and the manager's briefing notes. + + Self-Assessment - Key Achievements: {key_achievements} + Self-Assessment - Skill Development: {skill_development} + Self-Assessment - Strengths: {strengths} + Self-Assessment - Areas for Improvement: {areas_for_improvement} + Self-Assessment - Alignment with Career Goals: {alignment_with_career_goals} + Manager Briefing - Quantitative Performance: {quantitative_performance} + Manager Briefing - Perception Gap: {perception_gap} + Manager Briefing - Manager Context: {manager_context} + Manager Briefing - Tenure: {tenure} + Manager Briefing - Cross-Departmental Contributions: {cross_departmental_contributions} + Manager Briefing - Career Alignment Prompt: {career_alignment_prompt} + + Respond to the user's query: {user_input} + Keep your response focused on helping them prepare for the meeting, referencing the provided data points. + """ + ) + + response = query_llm(prompt, context) + st.session_state.prep_messages.append({"role": "assistant", "content": response}) + with st.chat_message("assistant"): + st.markdown(response) + + except oracledb.Error as db_err: + st.error(f"Database error: {db_err}") + except Exception as e: + st.error(f"Unexpected error: {e}") + finally: + if cursor: cursor.close() + if connection: connection.close() + + + +run_meeting_preparation_chatbot() diff --git a/ai/generative-ai-service/hr-goal-alignment/files/pages/self_assessment_chatbot.py b/ai/generative-ai-service/hr-goal-alignment/files/pages/self_assessment_chatbot.py new file mode 100644 index 000000000..34a5aa8f0 --- /dev/null +++ b/ai/generative-ai-service/hr-goal-alignment/files/pages/self_assessment_chatbot.py @@ -0,0 +1,210 @@ +# Copyright (c) 2025 Oracle and/or its affiliates. +import streamlit as st +import json +from langchain.prompts import PromptTemplate +from utils import ( + get_employee_id_by_name, + generate_transcript_docx, + detect_covered_topic +) +from goal_alignment_backend import ( + load_self_assessment_from_db, + load_manager_briefing_from_db, + query_llm +) +import oracledb +import config + +def run_self_assessment_chatbot(): + st.title("Self-Assessment Chatbot") + + if "sa_chat_history" not in st.session_state: + st.session_state.sa_chat_history = [] + if "sa_conversation_complete" not in st.session_state: + st.session_state.sa_conversation_complete = False + if "sa_covered_topics" not in st.session_state: + st.session_state.sa_covered_topics = [] + if "sa_interaction_count" not in st.session_state: + st.session_state.sa_interaction_count = 0 + if "sa_report" not in st.session_state: + st.session_state.sa_report = None + if "sa_employee_name" not in st.session_state: + st.session_state.sa_employee_name = "" + if "sa_employee_id" not in st.session_state: + st.session_state.sa_employee_id = "" + + if st.session_state.sa_report is None: + employee_name_input = st.text_input("Enter Employee Name:", key="sa_employee_input") + + if st.button("Start Self-Assessment Chat", key="sa_gather_data_btn"): + if not employee_name_input: + st.warning("Please enter an employee name.") + return + + connection = None + try: + with st.spinner("Connecting to database and gathering data..."): + connection = oracledb.connect( + user=config.DB_USER, + password=config.DB_PASSWORD, + dsn=config.DB_DSN + ) + employee_id = get_employee_id_by_name(connection, employee_name_input) + + if not employee_id: + st.error(f"Employee '{employee_name_input}' not found in the database.") + return + + st.session_state.sa_employee_id = employee_id + st.session_state.sa_employee_name = employee_name_input + + self_assessment = load_self_assessment_from_db(connection, employee_id) + manager_briefing = load_manager_briefing_from_db(connection, employee_id) + + if not self_assessment and not manager_briefing: + st.error(f"No self-assessment or manager briefing data found for {employee_name_input}.") + return + + st.session_state.sa_report = { + "self_assessment": self_assessment or {"message": "No self-assessment data found."}, + "manager_briefing": manager_briefing or {"message": "No manager briefing data found."}, + } + + quantitative_performance = st.session_state.sa_report.get("manager_briefing", {}).get( + "quantitative_performance", "your recent performance" + ) or "your recent performance" + + prompt = PromptTemplate( + input_variables=["employee_name", "quantitative_performance"], + template=""" + You are a career mentor chatbot guiding {employee_name}. + Your aim is to prepare the employee for their upcoming quarterly evaluation with their manager. + Start the conversation with: "Good morning, {employee_name}. We're approaching your quarterly self-evaluation. I've gathered some performance insights to help." + Continue by mentioning the following insight: {quantitative_performance} + Ask what specific changes might have contributed to this. Keep it under 150 words. Tone is light and conversational. + Important rules: do not combine asking a question and concluding in the same message. Only conclude once the user has indicated they are done. + Ensure you balance references between manager briefing and self-evaluation, surfacing gaps or complementary insights. + """ + ) + + initial_msg = query_llm(prompt, { + "employee_name": employee_name_input, + "quantitative_performance": quantitative_performance + }) + + st.session_state.sa_chat_history = [{"Chatbot": initial_msg}] + st.rerun() + + except Exception as e: + st.error(f"Error: {e}") + finally: + if connection: + connection.close() + + if st.session_state.sa_report: + st.subheader(f"Chat with Mentor for {st.session_state.sa_employee_name}") + + for exchange in st.session_state.sa_chat_history: + if "User" in exchange: + with st.chat_message("user", avatar="πŸ§‘πŸ½β€πŸ’Ό"): + st.write(exchange["User"]) + if "Chatbot" in exchange: + with st.chat_message("assistant", avatar="πŸ’¬"): + st.write(exchange["Chatbot"]) + + if st.button("Stop Conversation", key="sa_stop_btn"): + st.session_state.sa_conversation_complete = True + if not st.session_state.sa_chat_history[-1].get("Chatbot", "").startswith("Got it! Best of luck"): # type: ignore + st.session_state.sa_chat_history.append( + {"Chatbot": "Okay, ending the conversation here. Best of luck!"} + ) + st.rerun() + + user_input = st.chat_input("You:", key="sa_user_chat_input", disabled=st.session_state.sa_conversation_complete) + if user_input and not st.session_state.sa_conversation_complete: + st.session_state.sa_chat_history.append({"User": user_input}) + st.session_state.sa_interaction_count += 1 + + done_phrases = [ + "no i'm good", "no im good", "i'm good", "im good", + "no thanks", "no thank you", "that's all", "stop", "can we stop", "we're done", "we are done", "all set" + ] + + if any(phrase in user_input.lower() for phrase in done_phrases): + st.session_state.sa_chat_history.append({ + "Chatbot": "Got it! Best of luck in your meeting with your manager. I'll send a summary of this chat for your reference." + }) + st.session_state.sa_conversation_complete = True + st.rerun() + + history_str = "\n".join( + [f"{k}: {v}" for exchange in st.session_state.sa_chat_history[-5:] for k, v in exchange.items()] + ) + + prompt = PromptTemplate( + input_variables=["employee_name", "chat_input", "chat_history", "full_report", "covered_topics"], + template=""" + You are a helpful career mentor guiding {employee_name}. Your goal is to discuss their self-assessment and manager's briefing points to prepare them for an evaluation meeting. + + **Chat History (recent first):** + {chat_history} + + **Latest input from {employee_name}:** + {chat_input} + + **Reference Report Data (Self Assessment & Manager Briefing):** + {full_report} + + **Topics Already Covered (Do not revisit these):** + {covered_topics} + + [.. trimmed instructions ..] + """ + ) + + try: + response = query_llm(prompt, { + "employee_name": st.session_state.sa_employee_name, + "chat_input": user_input, + "chat_history": history_str, + "full_report": json.dumps(st.session_state.sa_report, indent=2), + "covered_topics": json.dumps(st.session_state.sa_covered_topics), + }) + + report_data = { + **st.session_state.sa_report.get("self_assessment", {}), + **st.session_state.sa_report.get("manager_briefing", {}) + } + topic = detect_covered_topic(response, report_data) + if topic and topic not in st.session_state.sa_covered_topics: + st.session_state.sa_covered_topics.append(topic) + + st.session_state.sa_chat_history.append({"Chatbot": response}) + if "Best of luck in your meeting" in response: + st.session_state.sa_conversation_complete = True + + if st.session_state.sa_interaction_count >= 10 and not st.session_state.sa_conversation_complete: + st.session_state.sa_chat_history.append({ + "Chatbot": "We've covered quite a bit. Is there anything else you'd like to discuss before we wrap up?" + }) + + except Exception as e: + st.error(f"Error from LLM: {e}") + st.session_state.sa_chat_history.append({ + "Chatbot": "Sorry, I encountered an error trying to generate a response." + }) + + st.rerun() + + if st.session_state.sa_conversation_complete and st.session_state.sa_chat_history: + file = generate_transcript_docx(st.session_state.sa_chat_history, st.session_state.sa_employee_name) + st.download_button( + label="πŸ“₯ Download Conversation Transcript", + data=file, + file_name=f"{st.session_state.sa_employee_name}_SelfAssessmentChat_transcript.docx", + mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document", + key="sa_download_btn" + ) +run_self_assessment_chatbot() +if __name__ == "__streamlit__": + run_self_assessment_chatbot() diff --git a/ai/generative-ai-service/hr-goal-alignment/files/requirements.txt b/ai/generative-ai-service/hr-goal-alignment/files/requirements.txt new file mode 100644 index 000000000..41174ad2f --- /dev/null +++ b/ai/generative-ai-service/hr-goal-alignment/files/requirements.txt @@ -0,0 +1,102 @@ +aiofiles==24.1.0 +aiohappyeyeballs==2.6.1 +aiohttp==3.11.14 +aiosignal==1.3.2 +altair==5.5.0 +annotated-types==0.7.0 +anyio==4.9.0 +attrs==25.3.0 +backoff==2.2.1 +beautifulsoup4==4.13.4 +blinker==1.9.0 +cachetools==5.5.2 +certifi==2025.1.31 +cffi==1.17.1 +chardet==5.2.0 +charset-normalizer==3.4.1 +circuitbreaker==1.4.0 +click==8.1.8 +cryptography==42.0.8 +dataclasses-json==0.6.7 +et_xmlfile==2.0.0 +eval_type_backport==0.2.2 +filetype==1.2.0 +frozenlist==1.5.0 +gitdb==4.0.12 +GitPython==3.1.44 +graphviz==0.20.3 +h11==0.14.0 +httpcore==1.0.7 +httpx==0.28.1 +httpx-sse==0.4.0 +idna==3.10 +Jinja2==3.1.6 +joblib==1.4.2 +jsonpatch==1.33 +jsonpointer==3.0.0 +jsonschema==4.23.0 +jsonschema-specifications==2024.10.1 +langchain==0.3.21 +langchain-community==0.3.20 +langchain-core==0.3.49 +langchain-text-splitters==0.3.7 +langdetect==1.0.9 +langsmith==0.3.19 +MarkupSafe==3.0.2 +marshmallow==3.26.1 +multidict==6.2.0 +mypy-extensions==1.0.0 +narwhals==1.32.0 +nest-asyncio==1.6.0 +networkx==3.4.2 +nltk==3.9.1 +numpy==2.2.4 +oci==2.149.1 +olefile==0.47 +openpyxl==3.1.5 +oracledb==3.0.0 +packaging==24.2 +pandas==2.2.3 +pillow==11.1.0 +pip==23.2.1 +propcache==0.3.1 +protobuf==5.29.4 +psutil==7.0.0 +pyarrow==19.0.1 +pycparser==2.22 +pydantic==2.11.3 +pydantic_core==2.33.1 +pydantic-settings==2.8.1 +pydeck==0.9.1 +pyOpenSSL==24.3.0 +python-dateutil==2.9.0.post0 +python-dotenv==1.1.0 +python-iso639==2025.2.18 +pytz==2025.2 +PyYAML==6.0.2 +RapidFuzz==3.13.0 +referencing==0.36.2 +regex==2024.11.6 +requests==2.32.3 +requests-toolbelt==1.0.0 +rpds-py==0.24.0 +setuptools==65.5.0 +six==1.17.0 +smmap==5.0.2 +sniffio==1.3.1 +soupsieve==2.7 +SQLAlchemy==2.0.40 +streamlit==1.44.0 +tenacity==9.0.0 +toml==0.10.2 +tornado==6.4.2 +tqdm==4.67.1 +typing_extensions==4.13.0 +typing-inspect==0.9.0 +typing-inspection==0.4.0 +tzdata==2025.2 +urllib3==2.3.0 +webencodings==0.5.1 +wrapt==1.17.2 +xlrd==2.0.1 +yarl==1.18.3 diff --git a/ai/generative-ai-service/hr-goal-alignment/files/scripts/create_tables.py b/ai/generative-ai-service/hr-goal-alignment/files/scripts/create_tables.py new file mode 100644 index 000000000..ffac7c21b --- /dev/null +++ b/ai/generative-ai-service/hr-goal-alignment/files/scripts/create_tables.py @@ -0,0 +1,90 @@ +# Copyright (c) 2025 Oracle and/or its affiliates. +import oracledb +import sys +import os + +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) +import config + +connection = None +cursor = None +try: + # Create a connection to the database + connection = oracledb.connect( + user=config.DB_USER, + password=config.DB_PASSWORD, + dsn=config.DB_DSN + ) + + # Create a cursor object + cursor = connection.cursor() + + # SQL scripts to create the tables + create_table_scripts = [ + """ + CREATE TABLE Employees ( + employee_id VARCHAR2(255) PRIMARY KEY, + name VARCHAR2(255), + role VARCHAR2(255), + manager_id VARCHAR2(255), + FOREIGN KEY (manager_id) REFERENCES Employees(employee_id) + ) + """, + """ + CREATE TABLE Goals ( + goal_id VARCHAR2(255) PRIMARY KEY, + employee_id VARCHAR2(255), + title VARCHAR2(255), + objective CLOB, + metrics CLOB, + timeline VARCHAR2(255), + FOREIGN KEY (employee_id) REFERENCES Employees(employee_id) + ) + """, + """ + CREATE TABLE Self_Assessments ( + assessment_id VARCHAR2(255) PRIMARY KEY, + employee_id VARCHAR2(255), + key_achievements CLOB, + skill_development CLOB, + strengths CLOB, + areas_for_improvement CLOB, + alignment_with_career_goals CLOB, + FOREIGN KEY (employee_id) REFERENCES Employees(employee_id) + ) + """, + """ + CREATE TABLE Manager_Briefings ( + briefing_id VARCHAR2(255) PRIMARY KEY, + employee_id VARCHAR2(255), + quantitative_performance CLOB, + perception_gap CLOB, + manager_context CLOB, + tenure CLOB, + cross_departmental_contributions CLOB, + career_alignment_prompt CLOB, + FOREIGN KEY (employee_id) REFERENCES Employees(employee_id) + ) + """ + ] + + # Execute the SQL scripts + for script in create_table_scripts: + cursor.execute(script) + + # Commit the changes + connection.commit() + + print("Tables created successfully!") + +except oracledb.Error as error: + print("Error creating tables:") + print(error) + +finally: + # Close the cursor and connection + if cursor: + cursor.close() + if connection: + connection.close() + print("Connection closed") diff --git a/ai/generative-ai-service/hr-goal-alignment/files/scripts/populate_demo_data.py b/ai/generative-ai-service/hr-goal-alignment/files/scripts/populate_demo_data.py new file mode 100644 index 000000000..688a54080 --- /dev/null +++ b/ai/generative-ai-service/hr-goal-alignment/files/scripts/populate_demo_data.py @@ -0,0 +1,437 @@ +# Copyright (c) 2025 Oracle and/or its affiliates. +import oracledb +import os +import sys +import datetime +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) +import config + +# --- Data Generation --- + +# Helper function to format goal details into CLOB fields +def format_goal_details(goal_data): + objective_str = goal_data.get('objective', '') + metrics_str = goal_data.get('metrics', '') + + if 'key_actions' in goal_data: + objective_str += "\n\nKey Actions:\n" + "\n".join([f"- {action}" for action in goal_data['key_actions']]) + if 'success_criteria' in goal_data: + metrics_str += "\n\nSuccess Criteria:\n" + "\n".join([f"- {criteria}" for criteria in goal_data['success_criteria']]) + + return objective_str.strip(), metrics_str.strip() + +# Define Employees (11 total) +employees_data = [ + # Level 1 + {'employee_id': 'emp_001', 'name': 'Eleanor Vance', 'role': 'CEO', 'manager_id': None}, + # Level 2 - HR Head + {'employee_id': 'emp_002', 'name': 'Ava Sharma', 'role': 'Head of Learning, Talent, Development, and Performance', 'manager_id': 'emp_001'}, + # Level 2 - Retail Head + {'employee_id': 'emp_010', 'name': 'Ben Carter', 'role': 'Head of Retail Banking', 'manager_id': 'emp_001'}, + # Level 3 - HR Managers reporting to Ava (emp_002) + {'employee_id': 'emp_003', 'name': 'Fatima Rossi', 'role': 'Line Manager', 'manager_id': 'emp_002'}, + {'employee_id': 'emp_004', 'name': 'George Martin', 'role': 'Head of Performance Management', 'manager_id': 'emp_002'}, + {'employee_id': 'emp_005', 'name': 'Isabelle Dubois', 'role': 'Head of Talent Management', 'manager_id': 'emp_002'}, + # Level 3 - Retail Manager reporting to Ben (emp_010) + {'employee_id': 'emp_011', 'name': 'Olivia Green', 'role': 'Regional Branch Manager', 'manager_id': 'emp_010'}, + # Level 4 - HR Team reporting to Fatima (emp_003) + {'employee_id': 'emp_006', 'name': 'David Lee', 'role': 'Head of Learning Content', 'manager_id': 'emp_003'}, + {'employee_id': 'emp_007', 'name': 'Maria Garcia', 'role': 'Head of Learning Coverage', 'manager_id': 'emp_003'}, + {'employee_id': 'emp_008', 'name': 'Kenji Tanaka', 'role': 'Head of Learning Admin', 'manager_id': 'emp_003'}, + # Level 4 - Retail Team reporting to Olivia (emp_011) + {'employee_id': 'emp_012', 'name': 'Sam Wilson', 'role': 'Loan Officer', 'manager_id': 'emp_011'}, +] + +# Define Goals from JSON and generate others +goals_data = [] +goal_counter = 1 + +# Process JSON data +json_data = { + "Fatima": { + "employee_id": "emp_003", + "goals": [ + { + "title": "Enhance Learning Programs", + "objective": "enhance learning programs", + "metrics": "better learning program effectiveness and completion", + "timeline": "Q2 2025" + } + ] + }, + "Head of Learning Content": { + "employee_id": "emp_006", + "goals": [ + { + "title": "Improve Curriculum Development", + "objective": "Develop high-quality, engaging, and relevant learning content for employees.", + "key_actions": [ + "Continue designing and updating curriculum based on employee needs and feedback.", + "Collaborate with internal subject matter experts (SMEs) to ensure content accuracy and relevancy." + ], + "success_criteria": [ + "Complete 100% of the curriculum updates scheduled for the year.", + "Achieve a satisfaction rating of at least 85% for new content via feedback surveys." + ], + "timeline": "Q2 - Q4 2025" + }, + { + "title": "Measure Effectiveness of Learning Content", + "objective": "Establish clear metrics and targets to measure the effectiveness of learning materials.", + "key_actions": [ + "Work with the Head of Learning Analytics (if applicable) to set clear KPIs for content effectiveness (e.g., post-training retention, completion rates).", + "Introduce mechanisms to collect feedback post-training, such as surveys or assessments.", + "Analyze feedback and revise content based on employee performance outcomes." + ], + "success_criteria": [ + "Set and track KPIs such as a 90%+ completion rate for each course.", + "Post-training assessment scores to increase by 10% from baseline." + ], + "timeline": "Ongoing, with a review every quarter" + }, + { + "title": "Align Content with Company Needs", + "objective": "Ensure that learning content is directly aligned with organizational goals and employee roles.", + "key_actions": [ + "Collaborate with leadership and department heads to identify key learning needs aligned with strategic goals.", + "Review and revise content quarterly to ensure alignment." + ], + "success_criteria": [ + "100% of learning content is updated to align with business objectives by end of the year." + ], + "timeline": "Ongoing with quarterly check-ins" + } + ] + }, + "Head of Learning Coverage": { + "employee_id": "emp_007", + "goals": [ + { + "title": "Expand Learning Coverage Across Geographies", + "objective": "Ensure that learning programs are accessible across all regions and geographies.", + "metrics": "100% regional coverage, 80% completion rate", + "timeline": "Q1 - Q3 2025" + }, + { + "title": "Set Completion Targets", + "objective": "Establish measurable completion targets for learning programs across all regions.", + "metrics": "90% completion rate for all employees in each geography", + "timeline": "Q4 2025" + }, + { + "title": "Improve Regional Feedback Mechanism", + "objective": "Collect region-specific feedback to ensure the learning content is relevant and engaging.", + "metrics": "80% feedback collected, 85% satisfaction rate", + "timeline": "Ongoing, review at the end of each quarter" + } + ] + }, + "Head of Learning Admin": { + "employee_id": "emp_008", + "goals": [ + { + "title": "Streamline Learning Administration", + "objective": "Improve the efficiency and organization of learning administration processes.", + "metrics": "25% reduction in manual tasks, 100% LMS tracking", + "timeline": "Q2 - Q3 2025" + }, + { + "title": "Improve Learning Program Completion Tracking", + "objective": "Ensure learning programs are completed on time and participation rates are tracked.", + "metrics": "90% completion rate, 95% on-time completion for mandatory programs", + "timeline": "Ongoing, with quarterly reviews" + }, + { + "title": "Link Administrative Metrics to Effectiveness", + "objective": "Align operational metrics with the effectiveness of learning programs.", + "metrics": "100% tracking of post-training performance, 80% retention rate", + "timeline": "Ongoing, review in Q4 2025" + } + ] + }, + "Head of Performance Management": { + "employee_id": "emp_004", + "goals": [ + { + "title": "Integrate Learning Outcomes into Performance Reviews", + "objective": "Ensure that learning achievements are considered during employee performance reviews.", + "metrics": "100% performance reviews include learning outcomes, 90% alignment", + "timeline": "By the end of Q2 2025" + }, + { + "title": "Measure Impact of Learning on Performance", + "objective": "Track the relationship between learning outcomes and performance improvements.", + "metrics": "80% of managers report performance improvements, 10% increase in performance reviews", + "timeline": "Ongoing, with bi-annual reviews" + }, + { + "title": "Align Learning and Performance Strategies", + "objective": "Ensure performance management systems and learning outcomes are working in tandem.", + "metrics": "Full alignment between performance review metrics and learning goals", + "timeline": "Ongoing, with review in Q3 2025" + } + ] + }, + "Head of Talent Management": { + "employee_id": "emp_005", + "goals": [ + { + "title": "Use Learning Data for Succession Planning", + "objective": "Leverage learning and development data to identify high-potential employees.", + "metrics": "Identify 5 high-potential employees, 75% linked to development programs", + "timeline": "Q1 - Q3 2025" + }, + { + "title": "Align Learning Programs with Talent Development Needs", + "objective": "Ensure learning and development programs are aligned with leadership and talent needs.", + "metrics": "100% alignment between learning and talent development goals", + "timeline": "Ongoing, with quarterly reviews" + }, + { + "title": "Increase the Use of Learning Data for Talent Reviews", + "objective": "Improve the use of learning data in talent reviews.", + "metrics": "90% of talent reviews include learning data, 85% leadership satisfaction", + "timeline": "Ongoing, with bi-annual reviews" + } + ] + }, + "Head of Learning, Talent, Development, and Performance": { + "employee_id": "emp_002", + "goals": [ + { + "title": "Improve Employee Capability Scores by 18%", + "objective": "Enhance employee skills and competencies across the organization", + "metrics": "18% improvement in employee capability scores, 85% training completion rate", + "timeline": "End of Q4 2025" + }, + { + "title": "Enhance Learning and Development Programs", + "objective": "Improve effectiveness and reach of learning programs across departments", + "metrics": "75% employee participation, 85% training satisfaction rate, 10% increase in internal promotions", + "timeline": "Ongoing, with review at the end of Q2 2025" + }, + { + "title": "Align Learning and Development with Business Goals", + "objective": "Ensure L&D initiatives align with strategic business goals", + "metrics": "100% alignment with business objectives, measurable impact on retention, productivity, and engagement", + "timeline": "Ongoing, with quarterly reviews" + } + ] + } +} + +for name, data in json_data.items(): + emp_id = data['employee_id'] + for goal in data['goals']: + obj, met = format_goal_details(goal) + goals_data.append({ + 'goal_id': f'goal_{goal_counter:03d}', + 'employee_id': emp_id, + 'title': goal['title'], + 'objective': obj, + 'metrics': met, + 'timeline': goal['timeline'] + }) + goal_counter += 1 + +# Generate goals for remaining employees +remaining_employees = ['emp_001', 'emp_010', 'emp_011', 'emp_012'] +generic_goals = { + 'emp_001': [{'title': 'Drive Overall Company Growth', 'objective': 'Achieve 15% YoY revenue growth.', 'metrics': 'Revenue figures, market share.', 'timeline': 'End of 2025'}], + 'emp_010': [{'title': 'Expand Retail Banking Market Share', 'objective': 'Increase market share by 5% in target regions.', 'metrics': 'Market share reports, customer acquisition rate.', 'timeline': 'End of 2025'}], + 'emp_011': [{'title': 'Improve Branch Performance', 'objective': 'Increase regional branch profitability by 10%.', 'metrics': 'Branch P&L statements, customer satisfaction scores.', 'timeline': 'End of Q3 2025'}], + 'emp_012': [{'title': 'Increase Loan Origination Volume', 'objective': 'Originate $5M in new loans.', 'metrics': 'Total loan value originated, approval rate.', 'timeline': 'End of Q2 2025'}], +} + +for emp_id in remaining_employees: + for goal in generic_goals[emp_id]: + goals_data.append({ + 'goal_id': f'goal_{goal_counter:03d}', + 'employee_id': emp_id, + 'title': goal['title'], + 'objective': goal['objective'], + 'metrics': goal['metrics'], + 'timeline': goal['timeline'] + }) + goal_counter += 1 + + +# Define Self Assessments (Generate for all) +self_assessments_data = [] +assessment_counter = 1 +for emp in employees_data: + self_assessments_data.append({ + 'assessment_id': f'assess_{assessment_counter:03d}', + 'employee_id': emp['employee_id'], + 'key_achievements': f"Successfully completed project X related to {emp['role']}.", + 'skill_development': f"Attended workshop on Advanced {emp['role']} Techniques.", + 'strengths': f"Strong analytical skills, effective communication within the {emp['role']} domain.", + 'areas_for_improvement': f"Need to improve delegation skills for {emp['role']} tasks.", + 'alignment_with_career_goals': f"Current role provides good experience towards long-term goal of becoming Senior {emp['role']}." + }) + assessment_counter += 1 + +# Define Manager Briefings (Generate for all except CEO) +manager_briefings_data = [] +briefing_counter = 1 +employee_map = {emp['employee_id']: emp for emp in employees_data} + +for emp in employees_data: + if emp['manager_id']: # Only generate for employees with managers + manager_name = employee_map.get(emp['manager_id'], {}).get('name', 'Their Manager') + manager_briefings_data.append({ + 'briefing_id': f'brief_{briefing_counter:03d}', + 'employee_id': emp['employee_id'], + 'quantitative_performance': f"Met 95% of KPIs for the last quarter in their role as {emp['role']}.", + 'perception_gap': f"{emp['name']} rated their teamwork skills slightly lower than peer feedback indicated.", + 'manager_context': f"Upcoming 1:1 discussion with {manager_name} to review Q1 goals.", + 'tenure': f"{datetime.date.today().year - 2022}-year tenure; focus on skill progression.", # Example tenure + 'cross_departmental_contributions': f"Collaborated with Marketing on the recent product launch.", + 'career_alignment_prompt': f"Discuss how current {emp['role']} objectives align with {emp['name']}'s stated long-term career aspirations." + }) + briefing_counter += 1 + + +# --- Database Insertion --- + +connection = None +cursor = None + +try: + print("Connecting to the database...") + connection = oracledb.connect( + user=config.DB_USER, + password=config.DB_PASSWORD, + dsn=config.DB_DSN + ) + print("Connection successful.") + cursor = connection.cursor() + + # Insert Employees + print("Inserting Employees...") + emp_sql = "INSERT INTO Employees (employee_id, name, role, manager_id) VALUES (:1, :2, :3, :4)" + cursor.executemany(emp_sql, [ + (e['employee_id'], e['name'], e['role'], e['manager_id']) for e in employees_data + ]) + print(f"{cursor.rowcount} Employees inserted.") + + # Insert Goals + print("Inserting Goals...") + goal_sql = "INSERT INTO Goals (goal_id, employee_id, title, objective, metrics, timeline) VALUES (:1, :2, :3, :4, :5, :6)" + # Prepare data, ensuring CLOBs are handled correctly + goal_tuples = [] + for g in goals_data: + objective_clob = cursor.var(oracledb.DB_TYPE_CLOB) + objective_clob.setvalue(0, g['objective']) + metrics_clob = cursor.var(oracledb.DB_TYPE_CLOB) + metrics_clob.setvalue(0, g['metrics']) + goal_tuples.append(( + g['goal_id'], g['employee_id'], g['title'], + objective_clob, metrics_clob, g['timeline'] + )) + # Using individual execute calls for robustness with CLOBs + inserted_goals = 0 + for data_tuple in goal_tuples: + try: + cursor.execute(goal_sql, data_tuple) + inserted_goals += 1 + except oracledb.Error as insert_error: + print(f"Error inserting goal {data_tuple[0]} for employee {data_tuple[1]}: {insert_error}") + # Decide whether to continue or break/rollback + # For now, we'll print and continue + print(f"{inserted_goals} Goals processed.") + + + # Insert Self Assessments + print("Inserting Self Assessments...") + sa_sql = """ + INSERT INTO Self_Assessments ( + assessment_id, employee_id, key_achievements, skill_development, + strengths, areas_for_improvement, alignment_with_career_goals + ) VALUES (:1, :2, :3, :4, :5, :6, :7) + """ + # Prepare data for CLOBs + sa_tuples = [] + for sa in self_assessments_data: + clob_vars = {k: cursor.var(oracledb.DB_TYPE_CLOB) for k in sa if k not in ['assessment_id', 'employee_id']} + for k, v in clob_vars.items(): + v.setvalue(0, sa[k]) + sa_tuples.append(( + sa['assessment_id'], sa['employee_id'], + clob_vars['key_achievements'], clob_vars['skill_development'], + clob_vars['strengths'], clob_vars['areas_for_improvement'], + clob_vars['alignment_with_career_goals'] + )) + # Using individual execute calls + inserted_assessments = 0 + for data_tuple in sa_tuples: + try: + cursor.execute(sa_sql, data_tuple) + inserted_assessments += 1 + except oracledb.Error as insert_error: + print(f"Error inserting assessment {data_tuple[0]} for employee {data_tuple[1]}: {insert_error}") + print(f"{inserted_assessments} Self Assessments processed.") + + + # Insert Manager Briefings + print("Inserting Manager Briefings...") + mb_sql = """ + INSERT INTO Manager_Briefings ( + briefing_id, employee_id, quantitative_performance, perception_gap, + manager_context, tenure, cross_departmental_contributions, career_alignment_prompt + ) VALUES (:1, :2, :3, :4, :5, :6, :7, :8) + """ + # Prepare data for CLOBs + mb_tuples = [] + for mb in manager_briefings_data: + clob_vars = {k: cursor.var(oracledb.DB_TYPE_CLOB) for k in mb if k not in ['briefing_id', 'employee_id']} + for k, v in clob_vars.items(): + v.setvalue(0, mb[k]) + mb_tuples.append(( + mb['briefing_id'], mb['employee_id'], + clob_vars['quantitative_performance'], clob_vars['perception_gap'], + clob_vars['manager_context'], clob_vars['tenure'], + clob_vars['cross_departmental_contributions'], clob_vars['career_alignment_prompt'] + )) + # Using individual execute calls + inserted_briefings = 0 + for data_tuple in mb_tuples: + try: + cursor.execute(mb_sql, data_tuple) + inserted_briefings += 1 + except oracledb.Error as insert_error: + print(f"Error inserting briefing {data_tuple[0]} for employee {data_tuple[1]}: {insert_error}") + print(f"{inserted_briefings} Manager Briefings processed.") + + + # Commit the changes + connection.commit() + print("Data committed successfully!") + +except oracledb.Error as error: + print("Error during database operation:") + print(error) + if connection: + connection.rollback() + print("Transaction rolled back.") + +except ImportError: + print("Error: Could not import the 'oracledb' library.") + print("Please ensure it is installed ('pip install oracledb') and accessible.") + print("Also ensure your 'config.py' file exists in the same directory and has the correct credentials (ORACLE_USERNAME, ORACLE_PASSWORD, ORACLE_DSN).") + +# Removed incorrect 'except config.Error' block +except Exception as e: + print(f"An unexpected error occurred: {e}") + if connection: + connection.rollback() + print("Transaction rolled back due to unexpected error.") + +finally: + # Close the cursor and connection + if cursor: + cursor.close() + print("Cursor closed.") + if connection: + connection.close() + print("Connection closed.") diff --git a/ai/generative-ai-service/hr-goal-alignment/files/utils.py b/ai/generative-ai-service/hr-goal-alignment/files/utils.py new file mode 100644 index 000000000..f21dfdd54 --- /dev/null +++ b/ai/generative-ai-service/hr-goal-alignment/files/utils.py @@ -0,0 +1,70 @@ +# Copyright (c) 2025 Oracle and/or its affiliates. +import streamlit as st +from io import BytesIO +from docx import Document +import oracledb + +# ----------------------- +# Database Helpers +# ----------------------- + +def get_employee_id_by_name(connection, name): + """Fetches employee_id from the database based on name.""" + try: + cursor = connection.cursor() + cursor.execute("SELECT employee_id FROM Employees WHERE name = :1", (name,)) + result = cursor.fetchone() + cursor.close() + if result: + return result[0] + except oracledb.Error as e: + st.error(f"Error fetching employee ID for {name}: {e}") + return None + +# ----------------------- +# Chat Transcript +# ----------------------- + +def generate_transcript_docx(chat_history, employee_name): + """Generates a DOCX transcript of the chat.""" + doc = Document() + doc.add_heading(f"Conversation Transcript with {employee_name}", level=1) + + for exchange in chat_history: + if "User" in exchange: + doc.add_paragraph(f"πŸ§‘πŸ½β€πŸ’Ό You: {exchange['User']}") + if "Chatbot" in exchange: + doc.add_paragraph(f"πŸ’¬ Chatbot: {exchange['Chatbot']}") + doc.add_paragraph("") # Spacer + + file_stream = BytesIO() + doc.save(file_stream) + file_stream.seek(0) + return file_stream + +# ----------------------- +# Topic Detection +# ----------------------- + +def detect_covered_topic(response_text, report_data): + """Detects which topic was covered in the chatbot response.""" + keywords_map = { + "quantitative_performance": ["delivery metrics", "improved", "performance metrics", "kpis"], + "perception_gap": ["perception gap", "rated", "client feedback", "communication skills", "teamwork skills"], + "manager_context": ["manager", "upcoming meeting", "context with manager", "1:1 discussion"], + "tenure": ["tenure", "years at company", "length of service", "skill progression"], + "cross_departmental_contributions": ["cross-department", "collaboration", "led project", "marketing"], + "career_alignment_prompt": ["career alignment", "long-term", "aspirations", "goals"], + "key_achievements": ["key achievements", "delivered projects", "task management", "completed project"], + "skill_development": ["skill development", "leadership skills", "improve in communication", "workshop"], + "strengths": ["strengths", "delegation", "task prioritization", "analytical skills"], + "areas_for_improvement": ["areas for improvement", "confidence", "communication", "delegation skills"], + "alignment_with_career_goals": ["career goals", "leadership abilities", "long-term goals", "senior role"] + } + + response_lower = response_text.lower() + for topic, keywords in keywords_map.items(): + if any(keyword in response_lower for keyword in keywords): + if topic in report_data.get("self_assessment", {}) or topic in report_data.get("manager_briefing", {}): + return topic + return None