├── .gitignore ├── 00-setup.ipynb ├── 01-agent.ipynb ├── 02-reasoning-engine.ipynb ├── 03-cleanup.ipynb ├── README.md ├── api ├── .gitignore ├── Dockerfile ├── README.md ├── deploy.sh ├── go.mod ├── go.sum └── main.go ├── course_content.jsonl ├── docker-compose.yml ├── experiment ├── 02-embedding.ipynb ├── 03-retriever.ipynb └── openllmmetry.ipynb ├── requirements.txt ├── scripts ├── grafana │ ├── dashboards │ │ └── dashboard.json │ └── provisioning │ │ ├── dashboards │ │ └── demo.yml │ │ └── datasources │ │ └── demo.yml ├── opentelemetry │ └── config.yaml └── prometheus │ └── prometheus.yml ├── ui ├── .gitignore ├── Dockerfile ├── deploy.sh ├── requirements.txt ├── server.py └── start.sh └── voice-agent ├── client.go ├── courses ├── client.go └── tools.go ├── go.mod ├── go.sum ├── index.html ├── interviews └── prompt.go ├── main.go ├── pg.go ├── server.go ├── store.go └── tools.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | 162 | ui/vars.yml -------------------------------------------------------------------------------- /00-setup.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Building AI Agent Bot With RAG, Langchain, and Reasoning Engine From Scratch\n", 8 | "\n", 9 | "## Setup\n", 10 | "\n", 11 | "* This notebook will walk you through some required setup that you need to do before starting with the materials.\n", 12 | "\n", 13 | "* It is highly recommended to use new virtual environment when running jupyter notebook for this workshop." 14 | ] 15 | }, 16 | { 17 | "cell_type": "markdown", 18 | "metadata": {}, 19 | "source": [ 20 | "## Required Software Installed Locally\n", 21 | "\n", 22 | "* Python version 3.9, 3.10, or 3.11. **Python3.12 will not work**.\n", 23 | "\n", 24 | "* If you are using VSCode, please install Jupyter Notebook extensions.\n", 25 | "\n", 26 | "* Jupyter notebook. Please follow this [installation guide](https://docs.jupyter.org/en/stable/install.html). You may choose whether you want to install classic jupyter notebook or jupyterlab (the next-gen web ui for jupyter)\n", 27 | "\n", 28 | " * [Classic jupyter notebook installation guide](https://docs.jupyter.org/en/stable/install/notebook-classic.html)\n", 29 | "\n", 30 | " * [Jupyterlab installation guide](https://jupyterlab.readthedocs.io/en/stable/getting_started/installation.html)\n", 31 | "\n", 32 | "* Google Cloud CLI. Please follow this [installation guide](https://cloud.google.com/sdk/docs/install-sdk)" 33 | ] 34 | }, 35 | { 36 | "cell_type": "markdown", 37 | "metadata": {}, 38 | "source": [ 39 | "### Installing dependencies" 40 | ] 41 | }, 42 | { 43 | "cell_type": "code", 44 | "execution_count": null, 45 | "metadata": {}, 46 | "outputs": [], 47 | "source": [ 48 | "%%writefile requirements.txt\n", 49 | "\n", 50 | "google-cloud-aiplatform\n", 51 | "google-cloud-aiplatform[langchain]\n", 52 | "google-cloud-aiplatform[reasoningengine]\n", 53 | "langchain\n", 54 | "langchain_core\n", 55 | "langchain_community\n", 56 | "langchain-google-vertexai==2.0.8\n", 57 | "cloudpickle\n", 58 | "pydantic==2.9.2\n", 59 | "langchain-google-community\n", 60 | "google-cloud-discoveryengine\n", 61 | "nest-asyncio\n", 62 | "asyncio==3.4.3\n", 63 | "asyncpg==0.29.0\n", 64 | "cloud-sql-python-connector[asyncpg]\n", 65 | "langchain-google-cloud-sql-pg\n", 66 | "numpy\n", 67 | "pandas\n", 68 | "pgvector\n", 69 | "psycopg2-binary\n", 70 | "langchain-openai\n", 71 | "langgraph\n", 72 | "traceloop-sdk\n", 73 | "opentelemetry-instrumentation-google-generativeai\n", 74 | "opentelemetry-instrumentation-langchain\n", 75 | "opentelemetry-instrumentation-vertexai\n", 76 | "python-dotenv" 77 | ] 78 | }, 79 | { 80 | "cell_type": "code", 81 | "execution_count": null, 82 | "metadata": {}, 83 | "outputs": [], 84 | "source": [ 85 | "from dotenv import load_dotenv\n", 86 | "load_dotenv()" 87 | ] 88 | }, 89 | { 90 | "cell_type": "code", 91 | "execution_count": null, 92 | "metadata": {}, 93 | "outputs": [], 94 | "source": [ 95 | "!pip install --upgrade -r requirements.txt" 96 | ] 97 | }, 98 | { 99 | "cell_type": "markdown", 100 | "metadata": {}, 101 | "source": [ 102 | "in case you are facing issue with installing psycopg2, please run the following command (linux only):\n", 103 | "\n", 104 | "```\n", 105 | "sudo apt update\n", 106 | "sudo apt install python3-dev libpq-dev\n", 107 | "```" 108 | ] 109 | }, 110 | { 111 | "cell_type": "markdown", 112 | "metadata": {}, 113 | "source": [ 114 | "You will require to restart the jupyter kernel once the dependency installed." 115 | ] 116 | }, 117 | { 118 | "cell_type": "code", 119 | "execution_count": null, 120 | "metadata": {}, 121 | "outputs": [], 122 | "source": [ 123 | "# import IPython\n", 124 | "\n", 125 | "# app = IPython.Application.instance()\n", 126 | "# app.kernel.do_shutdown(True)" 127 | ] 128 | }, 129 | { 130 | "cell_type": "markdown", 131 | "metadata": {}, 132 | "source": [ 133 | "## Setting up Google Cloud Account" 134 | ] 135 | }, 136 | { 137 | "cell_type": "markdown", 138 | "metadata": {}, 139 | "source": [ 140 | "#### Recommended account setup\n", 141 | "\n", 142 | "if you are running this in jupyter notebook locally, you may need to login to google cloud by running the following command from terminal:\n", 143 | "\n", 144 | "```\n", 145 | "gcloud auth login\n", 146 | "gcloud auth application-default login\n", 147 | "```" 148 | ] 149 | }, 150 | { 151 | "cell_type": "markdown", 152 | "metadata": {}, 153 | "source": [ 154 | "If you are using Google Colabs, you need to authenticate with your google account by running the following notebook cell. \n", 155 | "\n", 156 | "> Please remember that you will need to do this on each jupyter notebook during this workshop" 157 | ] 158 | }, 159 | { 160 | "cell_type": "code", 161 | "execution_count": 21, 162 | "metadata": {}, 163 | "outputs": [], 164 | "source": [ 165 | "# #@markdown ###Authenticate your Google Cloud Account and enable APIs.\n", 166 | "# # Authenticate gcloud.\n", 167 | "from google.colab import auth\n", 168 | "auth.authenticate_user()" 169 | ] 170 | }, 171 | { 172 | "cell_type": "markdown", 173 | "metadata": {}, 174 | "source": [ 175 | "## Accessing Google Cloud Credit\n", 176 | "\n", 177 | "Please redeem your $5 USD credit that you can use for this workshop. Link for this, will be shared on the class room.\n", 178 | "\n", 179 | "The instruction given will also require you to create a new GCP project. Create one!" 180 | ] 181 | }, 182 | { 183 | "cell_type": "markdown", 184 | "metadata": {}, 185 | "source": [ 186 | "## Enabling Google Service API\n", 187 | "\n", 188 | "Before creating cloud resources (e.g. database, cloudrun services, reasoning engine, etc), first we must enable the services api." 189 | ] 190 | }, 191 | { 192 | "cell_type": "code", 193 | "execution_count": null, 194 | "metadata": {}, 195 | "outputs": [], 196 | "source": [ 197 | "# @markdown Replace the required placeholder text below. You can modify any other default values, if you like.\n", 198 | "\n", 199 | "# please change the project id into your gcp project id you just created. \n", 200 | "project_id = \"imrenagi-gemini-experiment\" # @param {type:\"string\"}\n", 201 | "\n", 202 | "# you can leave this the same.\n", 203 | "region = \"us-central1\" # @param {type:\"string\"}\n", 204 | "\n", 205 | "!gcloud config set project {project_id} --quiet" 206 | ] 207 | }, 208 | { 209 | "cell_type": "code", 210 | "execution_count": null, 211 | "metadata": {}, 212 | "outputs": [], 213 | "source": [ 214 | "from googleapiclient import discovery\n", 215 | "service = discovery.build(\"cloudresourcemanager\", \"v1\")\n", 216 | "request = service.projects().get(projectId=project_id)\n", 217 | "response = request.execute()\n", 218 | "project_number = response[\"projectNumber\"]\n", 219 | "project_number" 220 | ] 221 | }, 222 | { 223 | "cell_type": "markdown", 224 | "metadata": {}, 225 | "source": [ 226 | "Here, we will enable few services:\n", 227 | "\n", 228 | "* `aiplatform.googleapis.com` -> used for using Gemini LLM and reasoning engine\n", 229 | "* `run.googleapis.com` -> used for deploying to cloud run\n", 230 | "* `cloudbuild.googleapis.com` -> used for building docker image and perform the deployment" 231 | ] 232 | }, 233 | { 234 | "cell_type": "code", 235 | "execution_count": null, 236 | "metadata": {}, 237 | "outputs": [], 238 | "source": [ 239 | "!gcloud services enable artifactregistry.googleapis.com\n", 240 | "!gcloud services enable compute.googleapis.com\n", 241 | "!gcloud services enable aiplatform.googleapis.com\n", 242 | "!gcloud services enable run.googleapis.com \n", 243 | "!gcloud services enable cloudbuild.googleapis.com\n", 244 | "!gcloud services enable sqladmin.googleapis.com\n", 245 | "!gcloud services enable cloudtrace.googleapis.com\n", 246 | "\n", 247 | "!gcloud beta services identity create --service=aiplatform.googleapis.com --project={project_id}\n", 248 | "\n", 249 | "!gcloud projects add-iam-policy-binding {project_id} \\\n", 250 | " --member=serviceAccount:{project_number}-compute@developer.gserviceaccount.com \\\n", 251 | " --role=\"roles/cloudbuild.builds.builder\"" 252 | ] 253 | }, 254 | { 255 | "cell_type": "markdown", 256 | "metadata": {}, 257 | "source": [ 258 | "# Deploying Dummy API server\n", 259 | "\n", 260 | "Later on this workshop, you will be using your AI agent to interact with api in order to get detail about an online course you provide as well as to create purchase request. Hence, we will deploy the simple stupid API to cloudrun.\n", 261 | "\n", 262 | "If you want to see the detail, you can check the `api/` directory.\n", 263 | "\n", 264 | "Now let's deploy the Go API to cloud run:" 265 | ] 266 | }, 267 | { 268 | "cell_type": "code", 269 | "execution_count": null, 270 | "metadata": {}, 271 | "outputs": [], 272 | "source": [ 273 | "# change this registry name with an unique name\n", 274 | "registry_name = \"imrenagi-gemini-experiment-registry\" # @param {type:\"string\"}\n", 275 | "\n", 276 | "!gcloud artifacts repositories create {registry_name} \\\n", 277 | " --repository-format=docker \\\n", 278 | " --location={region} \\\n", 279 | " --description=\"devfest artifact registry\" \\\n", 280 | " --immutable-tags \n", 281 | "\n", 282 | "registry_url = f\"{region}-docker.pkg.dev/{project_id}/{registry_name}\"" 283 | ] 284 | }, 285 | { 286 | "cell_type": "markdown", 287 | "metadata": {}, 288 | "source": [ 289 | "We will build the docker image used by the API" 290 | ] 291 | }, 292 | { 293 | "cell_type": "code", 294 | "execution_count": null, 295 | "metadata": {}, 296 | "outputs": [], 297 | "source": [ 298 | "!gcloud builds submit api --tag {registry_url}/courses-api" 299 | ] 300 | }, 301 | { 302 | "cell_type": "markdown", 303 | "metadata": {}, 304 | "source": [ 305 | "We will deploy the docker image to cloud run so that we can have the api up and running" 306 | ] 307 | }, 308 | { 309 | "cell_type": "code", 310 | "execution_count": null, 311 | "metadata": {}, 312 | "outputs": [], 313 | "source": [ 314 | "!gcloud run deploy courses-api --allow-unauthenticated --region {region} --quiet --image {registry_url}/courses-api" 315 | ] 316 | }, 317 | { 318 | "cell_type": "markdown", 319 | "metadata": {}, 320 | "source": [ 321 | "Once it is deployed, run the command to get the url of your dummy api. Take note because we will use it later:" 322 | ] 323 | }, 324 | { 325 | "cell_type": "code", 326 | "execution_count": null, 327 | "metadata": {}, 328 | "outputs": [], 329 | "source": [ 330 | "urls = !gcloud run services describe courses-api --region=us-central1 --format='value(status.url)'\n", 331 | "api_url = urls[0]\n", 332 | "print(api_url)" 333 | ] 334 | }, 335 | { 336 | "cell_type": "markdown", 337 | "metadata": {}, 338 | "source": [ 339 | "Testing the API" 340 | ] 341 | }, 342 | { 343 | "cell_type": "code", 344 | "execution_count": null, 345 | "metadata": {}, 346 | "outputs": [], 347 | "source": [ 348 | "!curl {api_url}/courses" 349 | ] 350 | }, 351 | { 352 | "cell_type": "markdown", 353 | "metadata": {}, 354 | "source": [ 355 | "# Creating Staging Bucket for AI Agent\n", 356 | "\n", 357 | "Later, when we deploy the AI Agent, we have to provide the staging gcs bucket used to store the pickle and some other configurations of our reasoning engine. So, let's create a new empty bucket. Please change `staging_bucket_name` variable below with globally unique name.\n", 358 | "\n", 359 | "Once the bucket created, take note the name of the bucket." 360 | ] 361 | }, 362 | { 363 | "cell_type": "code", 364 | "execution_count": null, 365 | "metadata": {}, 366 | "outputs": [], 367 | "source": [ 368 | "# change this with globaly unique name. you may add your name to make it unique. this bucket will be used later for storing the model\n", 369 | "staging_bucket_name = \"ai-agent-demo-bucket\" # @param {type:\"string\"}\n", 370 | "\n", 371 | "!gcloud storage buckets create gs://{staging_bucket_name} --project={project_id} --location={region} --uniform-bucket-level-access" 372 | ] 373 | }, 374 | { 375 | "cell_type": "markdown", 376 | "metadata": {}, 377 | "source": [ 378 | "# Data Preparation\n", 379 | "\n", 380 | "In this workshop, we are going to use written content from [OWASP CheatSheetSeries](https://github.com/OWASP/CheatSheetSeries) as the source document for our RAG. However, to reduce the cost, I already currated few files that we are going to use in `urls` variable. Instead of using all of them, we will just use few of them and build embedding with the currated files." 381 | ] 382 | }, 383 | { 384 | "cell_type": "markdown", 385 | "metadata": {}, 386 | "source": [ 387 | "The source code below will just iterate over all files within `sources` directory and create a `course_content.jsonl` file containing the file contents." 388 | ] 389 | }, 390 | { 391 | "cell_type": "code", 392 | "execution_count": null, 393 | "metadata": {}, 394 | "outputs": [], 395 | "source": [ 396 | "import json\n", 397 | "import uuid\n", 398 | "import requests\n", 399 | "from pathlib import Path\n", 400 | "\n", 401 | "urls = [\n", 402 | " \"https://raw.githubusercontent.com/OWASP/CheatSheetSeries/refs/heads/master/cheatsheets/Authentication_Cheat_Sheet.md\",\n", 403 | " \"https://raw.githubusercontent.com/OWASP/CheatSheetSeries/refs/heads/master/cheatsheets/Authorization_Cheat_Sheet.md\",\n", 404 | " \"https://raw.githubusercontent.com/OWASP/CheatSheetSeries/refs/heads/master/cheatsheets/File_Upload_Cheat_Sheet.md\",\n", 405 | " \"https://raw.githubusercontent.com/OWASP/CheatSheetSeries/refs/heads/master/cheatsheets/Forgot_Password_Cheat_Sheet.md\",\n", 406 | " \"https://raw.githubusercontent.com/OWASP/CheatSheetSeries/refs/heads/master/cheatsheets/Password_Storage_Cheat_Sheet.md\",\n", 407 | " \"https://raw.githubusercontent.com/OWASP/CheatSheetSeries/refs/heads/master/cheatsheets/REST_Security_Cheat_Sheet.md\",\n", 408 | " \"https://raw.githubusercontent.com/OWASP/CheatSheetSeries/refs/heads/master/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.md\"\n", 409 | "]\n", 410 | "\n", 411 | "def generate_course_content_jsonl():\n", 412 | " output_file = 'course_content.jsonl'\n", 413 | " \n", 414 | " with open(output_file, 'w') as jsonl_file:\n", 415 | "\n", 416 | " for url in urls:\n", 417 | " response = requests.get(url)\n", 418 | " if response.status_code == 200:\n", 419 | " content = response.text\n", 420 | " filename = url.split('/')[-1] \n", 421 | " title = filename.replace('_', ' ').replace('.md', '')\n", 422 | "\n", 423 | " slug = title.lower().replace(' ', '-')\n", 424 | " \n", 425 | " record = {\n", 426 | " 'id': str(uuid.uuid4()),\n", 427 | " 'title': title,\n", 428 | " 'content': content,\n", 429 | " 'file_path': str(url),\n", 430 | " 'slug': slug\n", 431 | " } \n", 432 | " json.dump(record, jsonl_file)\n", 433 | " jsonl_file.write('\\n')\n", 434 | " else:\n", 435 | " print(f\"Failed to download content. Status code: {response.status_code}\")\n", 436 | "\n", 437 | " \n", 438 | " print(f\"JSONL file '{output_file}' has been generated successfully.\")\n", 439 | "\n", 440 | "generate_course_content_jsonl()\n" 441 | ] 442 | }, 443 | { 444 | "cell_type": "markdown", 445 | "metadata": {}, 446 | "source": [ 447 | "Let's see what is inside the `course_content.jsonl` file:" 448 | ] 449 | }, 450 | { 451 | "cell_type": "code", 452 | "execution_count": null, 453 | "metadata": {}, 454 | "outputs": [], 455 | "source": [ 456 | "import pandas as pd\n", 457 | "\n", 458 | "df = pd.read_json('course_content.jsonl', lines=True)\n", 459 | "df.head()" 460 | ] 461 | }, 462 | { 463 | "cell_type": "markdown", 464 | "metadata": {}, 465 | "source": [ 466 | "# Creating Embedding and Vector Store\n", 467 | "\n", 468 | "This notebook demonstrates the process of creating embeddings and setting up a vector store for a course content retrieval system. \n", 469 | "\n", 470 | "It covers the following key steps:\n", 471 | "\n", 472 | "1. Importing necessary libraries and creating and setting up database and its configurations\n", 473 | "1. Connecting to either a Google Cloud SQL\n", 474 | "1. Loading course content data from markdown files\n", 475 | "1. Creating embeddings for the course content using a Google Gemini embedding model\n", 476 | "1. Storing the embeddings in a vector database for efficient similarity search" 477 | ] 478 | }, 479 | { 480 | "cell_type": "markdown", 481 | "metadata": {}, 482 | "source": [ 483 | "Setting up few constants:" 484 | ] 485 | }, 486 | { 487 | "cell_type": "code", 488 | "execution_count": 17, 489 | "metadata": {}, 490 | "outputs": [], 491 | "source": [ 492 | "import os\n", 493 | "instance_name = os.environ['CLOUDSQL_INSTANCE_NAME']\n", 494 | "database_password = os.environ['DB_PASSWORD']\n", 495 | "database_name = os.environ['DB_NAME']\n", 496 | "database_user = os.environ['DB_USER']\n", 497 | "\n", 498 | "# Dont update these lines below\n", 499 | "\n", 500 | "embeddings_table_name = \"course_content_embeddings\"\n", 501 | "chat_history_table_name = \"chat_histories\"\n", 502 | "gemini_embedding_model = \"text-embedding-004\"\n", 503 | "\n", 504 | "assert database_name, \"⚠️ Please provide a database name\"\n", 505 | "assert database_user, \"⚠️ Please provide a database user\"\n", 506 | "assert database_password, \"⚠️ Please provide a database password\"\n" 507 | ] 508 | }, 509 | { 510 | "cell_type": "markdown", 511 | "metadata": {}, 512 | "source": [ 513 | "## Setting Up PostgreSQL in Google Cloud SQL\n", 514 | "\n", 515 | "Here will we set the default GCP project and get information about the user using the GCP account." 516 | ] 517 | }, 518 | { 519 | "cell_type": "code", 520 | "execution_count": null, 521 | "metadata": {}, 522 | "outputs": [], 523 | "source": [ 524 | "# Grant Cloud SQL Client role to authenticated user\n", 525 | "current_user = !gcloud auth list --filter=status:ACTIVE --format=\"value(account)\"\n", 526 | "print(f\"{current_user}\")" 527 | ] 528 | }, 529 | { 530 | "cell_type": "markdown", 531 | "metadata": {}, 532 | "source": [ 533 | "Before sending query to database, we will have to add required permissions for our notebook so that it can access the database:" 534 | ] 535 | }, 536 | { 537 | "cell_type": "code", 538 | "execution_count": null, 539 | "metadata": {}, 540 | "outputs": [], 541 | "source": [ 542 | "print(f\"Granting Cloud SQL Client role to {current_user[0]}\")\n", 543 | "# granting cloudsql client role to the current user\n", 544 | "!gcloud projects add-iam-policy-binding {project_id} \\\n", 545 | " --member=user:{current_user[0]} \\\n", 546 | " --role=\"roles/cloudsql.client\"" 547 | ] 548 | }, 549 | { 550 | "cell_type": "markdown", 551 | "metadata": {}, 552 | "source": [ 553 | "Next, we are going to create new postgresql database from Google CloudSQL and create postgresql user/role which will be used to store the embeddings later on" 554 | ] 555 | }, 556 | { 557 | "cell_type": "code", 558 | "execution_count": null, 559 | "metadata": {}, 560 | "outputs": [], 561 | "source": [ 562 | "#@markdown Create and setup a Cloud SQL PostgreSQL instance, if not done already.\n", 563 | "database_version = !gcloud sql instances describe {instance_name} --format=\"value(databaseVersion)\"\n", 564 | "if database_version[0].startswith(\"POSTGRES\"):\n", 565 | " print(\"Found an existing Postgres Cloud SQL Instance!\")\n", 566 | "else:\n", 567 | " print(\"Creating new Cloud SQL instance...\")\n", 568 | " !gcloud sql instances create {instance_name} --database-version=POSTGRES_15 \\\n", 569 | " --region={region} --cpu=1 --memory=4GB --root-password={database_password} \\\n", 570 | " --authorized-networks=0.0.0.0/0\n", 571 | "# Create the database, if it does not exist.\n", 572 | "out = !gcloud sql databases list --instance={instance_name} --filter=\"NAME:{database_name}\" --format=\"value(NAME)\"\n", 573 | "if ''.join(out) == database_name:\n", 574 | " print(\"Database %s already exists, skipping creation.\" % database_name)\n", 575 | "else:\n", 576 | " !gcloud sql databases create {database_name} --instance={instance_name}\n", 577 | "# Create the database user for accessing the database.\n", 578 | "!gcloud sql users create {database_user} \\\n", 579 | " --instance={instance_name} \\\n", 580 | " --password={database_password}" 581 | ] 582 | }, 583 | { 584 | "cell_type": "markdown", 585 | "metadata": {}, 586 | "source": [ 587 | "Here we are going to get the ip of postgresql we just created. Take note to the database host ip address." 588 | ] 589 | }, 590 | { 591 | "cell_type": "code", 592 | "execution_count": null, 593 | "metadata": {}, 594 | "outputs": [], 595 | "source": [ 596 | "# get the ip address of the instance\n", 597 | "ip_addresses = !gcloud sql instances describe {instance_name} --project {project_id} --format 'value(ipAddresses.ipAddress)'\n", 598 | "# Split the IP addresses and take the first one\n", 599 | "database_host = ip_addresses[0].split(';')[0].strip()\n", 600 | "print(f\"Using database host: {database_host}\")" 601 | ] 602 | }, 603 | { 604 | "cell_type": "markdown", 605 | "metadata": {}, 606 | "source": [ 607 | "## Prepare the embeddings\n", 608 | "\n", 609 | "Now, we will build the embeddings from the content we have selected. " 610 | ] 611 | }, 612 | { 613 | "cell_type": "markdown", 614 | "metadata": {}, 615 | "source": [ 616 | "Before creating the embedding, we need to split the content of each files into chunks. This is most of the time required, especially when the content is toolong, because embedding has the limit for the number of input token it can accept." 617 | ] 618 | }, 619 | { 620 | "cell_type": "code", 621 | "execution_count": null, 622 | "metadata": {}, 623 | "outputs": [], 624 | "source": [ 625 | "from langchain.text_splitter import MarkdownTextSplitter\n", 626 | "\n", 627 | "text_splitter = MarkdownTextSplitter(\n", 628 | " chunk_size=1000, \n", 629 | " chunk_overlap=200)\n", 630 | "\n", 631 | "from langchain_core.documents import Document\n", 632 | "\n", 633 | "chunked = []\n", 634 | "for index, row in df.iterrows():\n", 635 | " course_content_id = row[\"id\"]\n", 636 | " title = row[\"title\"]\n", 637 | " content = row[\"content\"]\n", 638 | " splits = text_splitter.create_documents([content])\n", 639 | " for s in splits:\n", 640 | " metadata = {\"course_content_id\": course_content_id, \"title\": title}\n", 641 | " doc = Document(page_content=s.page_content, metadata=metadata)\n", 642 | " chunked.append(doc)\n", 643 | "\n", 644 | "chunked[0]" 645 | ] 646 | }, 647 | { 648 | "cell_type": "code", 649 | "execution_count": null, 650 | "metadata": {}, 651 | "outputs": [], 652 | "source": [ 653 | "len(chunked)" 654 | ] 655 | }, 656 | { 657 | "cell_type": "markdown", 658 | "metadata": {}, 659 | "source": [ 660 | "Once we have the file content chunked into smaller sizes, we are going to create embedding for each chunked and store it to cloudsql.\n", 661 | "\n", 662 | "Now let's initialize vertex ai sdk and create the embedding services." 663 | ] 664 | }, 665 | { 666 | "cell_type": "code", 667 | "execution_count": null, 668 | "metadata": {}, 669 | "outputs": [], 670 | "source": [ 671 | "from langchain_google_vertexai import VertexAIEmbeddings\n", 672 | "import vertexai\n", 673 | "\n", 674 | "print(project_id)\n", 675 | "# Initialize Vertex AI\n", 676 | "vertexai.init(project=project_id, location=region)\n", 677 | "# Create a Vertex AI Embeddings service\n", 678 | "embeddings_service = VertexAIEmbeddings(model_name=gemini_embedding_model)" 679 | ] 680 | }, 681 | { 682 | "cell_type": "markdown", 683 | "metadata": {}, 684 | "source": [ 685 | "Now, let's construct the embeddings and store it to the database.\n", 686 | "\n", 687 | "On the function below we are doing these steps:\n", 688 | "1. We are initiating a PostgresEngine. This instance of PostgresEngine will be used to handle database connection as well as authentication.\n", 689 | "1. Then, `ainit_vectorstore_table()` will create a table which will be used to store the chucked content, its embedding, and metadata.\n", 690 | "1. We initialize the PostgresVectorStore and provide the engine as well as the embedding service.\n", 691 | "1. For each chunked document, we call function `aadd_documents` to create embedding and create new record on the given table." 692 | ] 693 | }, 694 | { 695 | "cell_type": "code", 696 | "execution_count": null, 697 | "metadata": {}, 698 | "outputs": [], 699 | "source": [ 700 | "!pip install pip-system-certs\n", 701 | "!pip install --upgrade certifi urllib3" 702 | ] 703 | }, 704 | { 705 | "cell_type": "code", 706 | "execution_count": null, 707 | "metadata": {}, 708 | "outputs": [], 709 | "source": [ 710 | "from langchain_google_cloud_sql_pg import PostgresEngine, PostgresVectorStore\n", 711 | "import uuid\n", 712 | "\n", 713 | "async def create_vectorstore():\n", 714 | " engine = await PostgresEngine.afrom_instance(\n", 715 | " project_id,\n", 716 | " region,\n", 717 | " instance_name,\n", 718 | " database_name,\n", 719 | " user=database_user,\n", 720 | " password=database_password,\n", 721 | " )\n", 722 | "\n", 723 | " await engine.ainit_chat_history_table(\n", 724 | " table_name=chat_history_table_name\n", 725 | " )\n", 726 | "\n", 727 | " await engine.ainit_vectorstore_table(\n", 728 | " table_name=embeddings_table_name, vector_size=768, overwrite_existing=True\n", 729 | " )\n", 730 | "\n", 731 | " vector_store = await PostgresVectorStore.create(\n", 732 | " engine,\n", 733 | " table_name=embeddings_table_name,\n", 734 | " embedding_service=embeddings_service,\n", 735 | " )\n", 736 | "\n", 737 | " ids = [str(uuid.uuid4()) for i in range(len(chunked))]\n", 738 | " await vector_store.aadd_documents(chunked, ids=ids)\n", 739 | "\n", 740 | "await create_vectorstore()" 741 | ] 742 | }, 743 | { 744 | "cell_type": "markdown", 745 | "metadata": {}, 746 | "source": [ 747 | "Once you have the vector store, you can check the content from google cloud sql data viewer." 748 | ] 749 | }, 750 | { 751 | "cell_type": "markdown", 752 | "metadata": {}, 753 | "source": [ 754 | "# Retriever\n", 755 | "\n", 756 | "Once we have data stored in cloudsql, we need to find a way to query the data. This notebook covers how we can create and use the postgresql retriever to perform similarity search." 757 | ] 758 | }, 759 | { 760 | "cell_type": "markdown", 761 | "metadata": {}, 762 | "source": [ 763 | "Similar to previous section, we will try to create PostgresEngine to connect to CloudSQL instance:" 764 | ] 765 | }, 766 | { 767 | "cell_type": "code", 768 | "execution_count": 17, 769 | "metadata": {}, 770 | "outputs": [], 771 | "source": [ 772 | "from langchain_google_cloud_sql_pg import PostgresEngine\n", 773 | "\n", 774 | "pg_engine = PostgresEngine.from_instance(\n", 775 | " project_id=project_id,\n", 776 | " instance=instance_name,\n", 777 | " region=region,\n", 778 | " database=database_name,\n", 779 | " user=database_password,\n", 780 | " password=database_password,\n", 781 | ")" 782 | ] 783 | }, 784 | { 785 | "cell_type": "markdown", 786 | "metadata": {}, 787 | "source": [ 788 | "We create the vector store object by using the engine and embedding service we created earlier:" 789 | ] 790 | }, 791 | { 792 | "cell_type": "code", 793 | "execution_count": 18, 794 | "metadata": {}, 795 | "outputs": [], 796 | "source": [ 797 | "from langchain_google_cloud_sql_pg import PostgresVectorStore\n", 798 | "\n", 799 | "vector_store = PostgresVectorStore.create_sync(\n", 800 | " pg_engine,\n", 801 | " table_name=embeddings_table_name,\n", 802 | " embedding_service=embeddings_service,\n", 803 | " )\n", 804 | "retriever = vector_store.as_retriever(search_kwargs={\"k\": 10})" 805 | ] 806 | }, 807 | { 808 | "cell_type": "markdown", 809 | "metadata": {}, 810 | "source": [ 811 | "Let's try with some query:" 812 | ] 813 | }, 814 | { 815 | "cell_type": "code", 816 | "execution_count": null, 817 | "metadata": {}, 818 | "outputs": [], 819 | "source": [ 820 | "retriever.invoke(\"how to design forgot password?\")" 821 | ] 822 | }, 823 | { 824 | "cell_type": "code", 825 | "execution_count": null, 826 | "metadata": {}, 827 | "outputs": [], 828 | "source": [ 829 | "retriever.invoke(\"how to design security for authentication?\")" 830 | ] 831 | } 832 | ], 833 | "metadata": { 834 | "kernelspec": { 835 | "display_name": ".venv", 836 | "language": "python", 837 | "name": "python3" 838 | }, 839 | "language_info": { 840 | "codemirror_mode": { 841 | "name": "ipython", 842 | "version": 3 843 | }, 844 | "file_extension": ".py", 845 | "mimetype": "text/x-python", 846 | "name": "python", 847 | "nbconvert_exporter": "python", 848 | "pygments_lexer": "ipython3", 849 | "version": "3.11.6" 850 | } 851 | }, 852 | "nbformat": 4, 853 | "nbformat_minor": 2 854 | } 855 | -------------------------------------------------------------------------------- /01-agent.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Building agent prototype\n", 8 | "\n", 9 | "This notebook covers how we can do prototyping with ai agent. This agent will have capabilities:\n", 10 | "\n", 11 | "* To use postgres vector store we created earlier to get some information about the course.\n", 12 | "* To call API server we deployed earlier to get information about the courses (name, price, etc) and to make payment url." 13 | ] 14 | }, 15 | { 16 | "cell_type": "markdown", 17 | "metadata": {}, 18 | "source": [ 19 | "## Required Software Installed Locally\n", 20 | "\n", 21 | "* Python version 3.9, 3.10, or 3.11. **Python3.12 will not work**.\n", 22 | "\n", 23 | "* If you are using VSCode, please install Jupyter Notebook extensions.\n", 24 | "\n", 25 | "* Jupyter notebook. Please follow this [installation guide](https://docs.jupyter.org/en/stable/install.html). You may choose whether you want to install classic jupyter notebook or jupyterlab (the next-gen web ui for jupyter)\n", 26 | "\n", 27 | " * [Classic jupyter notebook installation guide](https://docs.jupyter.org/en/stable/install/notebook-classic.html)\n", 28 | "\n", 29 | " * [Jupyterlab installation guide](https://jupyterlab.readthedocs.io/en/stable/getting_started/installation.html)\n", 30 | "\n", 31 | "* Google Cloud CLI. Please follow this [installation guide](https://cloud.google.com/sdk/docs/install-sdk)" 32 | ] 33 | }, 34 | { 35 | "cell_type": "markdown", 36 | "metadata": {}, 37 | "source": [ 38 | "### Installing dependencies" 39 | ] 40 | }, 41 | { 42 | "cell_type": "code", 43 | "execution_count": null, 44 | "metadata": {}, 45 | "outputs": [], 46 | "source": [ 47 | "%%writefile requirements.txt\n", 48 | "\n", 49 | "google-cloud-aiplatform\n", 50 | "google-cloud-aiplatform[langchain]\n", 51 | "google-cloud-aiplatform[reasoningengine]\n", 52 | "langchain\n", 53 | "langchain_core\n", 54 | "langchain_community\n", 55 | "langchain-google-vertexai==2.0.8\n", 56 | "cloudpickle\n", 57 | "pydantic==2.9.2\n", 58 | "langchain-google-community\n", 59 | "google-cloud-discoveryengine\n", 60 | "nest-asyncio\n", 61 | "asyncio==3.4.3\n", 62 | "asyncpg==0.29.0\n", 63 | "cloud-sql-python-connector[asyncpg]\n", 64 | "langchain-google-cloud-sql-pg\n", 65 | "numpy\n", 66 | "pandas\n", 67 | "pgvector\n", 68 | "psycopg2-binary\n", 69 | "langchain-openai\n", 70 | "langgraph\n", 71 | "traceloop-sdk\n", 72 | "opentelemetry-instrumentation-google-generativeai\n", 73 | "opentelemetry-instrumentation-langchain\n", 74 | "opentelemetry-instrumentation-vertexai\n", 75 | "python-dotenv" 76 | ] 77 | }, 78 | { 79 | "cell_type": "code", 80 | "execution_count": null, 81 | "metadata": {}, 82 | "outputs": [], 83 | "source": [ 84 | "!pip install --upgrade -r requirements.txt" 85 | ] 86 | }, 87 | { 88 | "cell_type": "code", 89 | "execution_count": null, 90 | "metadata": {}, 91 | "outputs": [], 92 | "source": [ 93 | "from dotenv import load_dotenv\n", 94 | "load_dotenv()" 95 | ] 96 | }, 97 | { 98 | "cell_type": "markdown", 99 | "metadata": {}, 100 | "source": [ 101 | "## Setting up Google Cloud Account" 102 | ] 103 | }, 104 | { 105 | "cell_type": "markdown", 106 | "metadata": {}, 107 | "source": [ 108 | "#### Recommended account setup\n", 109 | "\n", 110 | "if you are running this in jupyter notebook locally, you may need to login to google cloud by running the following command from terminal:\n", 111 | "\n", 112 | "```\n", 113 | "gcloud auth login\n", 114 | "gcloud auth application-default login\n", 115 | "```" 116 | ] 117 | }, 118 | { 119 | "cell_type": "markdown", 120 | "metadata": {}, 121 | "source": [ 122 | "If you are using Google Colabs, you need to authenticate with your google account by running the following notebook cell. \n", 123 | "\n", 124 | "> Please remember that you will need to do this on each jupyter notebook during this workshop" 125 | ] 126 | }, 127 | { 128 | "cell_type": "code", 129 | "execution_count": null, 130 | "metadata": {}, 131 | "outputs": [], 132 | "source": [ 133 | "# #@markdown ###Authenticate your Google Cloud Account and enable APIs.\n", 134 | "# # Authenticate gcloud.\n", 135 | "from google.colab import auth\n", 136 | "auth.authenticate_user()" 137 | ] 138 | }, 139 | { 140 | "cell_type": "markdown", 141 | "metadata": {}, 142 | "source": [ 143 | "Let's start with importhing few stuff:" 144 | ] 145 | }, 146 | { 147 | "cell_type": "code", 148 | "execution_count": null, 149 | "metadata": {}, 150 | "outputs": [], 151 | "source": [ 152 | "from IPython.display import display, Markdown\n", 153 | "\n", 154 | "from langchain.agents.format_scratchpad import format_to_openai_function_messages\n", 155 | "from langchain.agents import tool\n", 156 | "from langchain.pydantic_v1 import BaseModel, Field\n", 157 | "\n", 158 | "from langchain.memory import ChatMessageHistory\n", 159 | "from langchain_community.chat_message_histories import ChatMessageHistory\n", 160 | "from langchain_core.chat_history import BaseChatMessageHistory\n", 161 | "\n", 162 | "from langchain.prompts import (\n", 163 | " ChatPromptTemplate,\n", 164 | " HumanMessagePromptTemplate,\n", 165 | " MessagesPlaceholder,\n", 166 | " SystemMessagePromptTemplate,\n", 167 | ")\n", 168 | "\n", 169 | "import pandas as pd\n", 170 | "from vertexai.preview import reasoning_engines\n", 171 | "from langchain_google_vertexai import HarmBlockThreshold, HarmCategory\n", 172 | "import requests" 173 | ] 174 | }, 175 | { 176 | "cell_type": "markdown", 177 | "metadata": {}, 178 | "source": [ 179 | "Let's define some variable. Please update these following variables according to your setup:\n", 180 | "* `project_id`\n", 181 | "* `region`\n", 182 | "* `staging_bucket_name`\n", 183 | "* `instance_name`, `database_password`, `database_name`, `database_user`" 184 | ] 185 | }, 186 | { 187 | "cell_type": "code", 188 | "execution_count": null, 189 | "metadata": {}, 190 | "outputs": [], 191 | "source": [ 192 | "project_id = \"imrenagi-devfest-2024\" # @param {type:\"string\"}\n", 193 | "region = \"us-central1\" #change this to project location\n", 194 | "staging_bucket_name = \"devfest24-demo-bucket\" # @param {type:\"string\"} #change this with your staging bucket name\n", 195 | "\n", 196 | "import os\n", 197 | "instance_name = os.environ['CLOUDSQL_INSTANCE_NAME']\n", 198 | "database_password = os.environ['DB_PASSWORD']\n", 199 | "database_name = os.environ['DB_NAME']\n", 200 | "database_user = os.environ['DB_USER']\n", 201 | "\n", 202 | "assert database_name, \"⚠️ Please provide a database name\"\n", 203 | "assert database_user, \"⚠️ Please provide a database user\"\n", 204 | "assert database_password, \"⚠️ Please provide a database password\"\n", 205 | "\n", 206 | "# dont update variable below\n", 207 | "\n", 208 | "!gcloud config set project {project_id} --quiet\n", 209 | "\n", 210 | "cloudrun_services = !gcloud run services describe courses-api --region=us-central1 --format='value(status.url)'\n", 211 | "api_base_url = cloudrun_services[0]\n", 212 | "\n", 213 | "staging_bucket_uri = f\"gs://{staging_bucket_name}\"\n", 214 | "# get the ip address of the cloudsql instance\n", 215 | "ip_addresses = !gcloud sql instances describe {instance_name} --format=\"value(ipAddresses[0].ipAddress)\"\n", 216 | "database_host = ip_addresses[0]\n", 217 | "\n", 218 | "gemini_embedding_model = \"text-embedding-004\"\n", 219 | "gemini_llm_model = \"gemini-1.5-pro\"\n", 220 | "embeddings_table_name = \"course_content_embeddings\"\n", 221 | "chat_history_table_name = \"chat_histories\"\n", 222 | "\n", 223 | "print(f\"API Base URL: {api_base_url}\")\n", 224 | "print(f\"Database Host: {database_host}\")" 225 | ] 226 | }, 227 | { 228 | "cell_type": "markdown", 229 | "metadata": {}, 230 | "source": [ 231 | "Let's initialize vertex ai, postgres engine and vector store. This is very similar to the previous module. But instead of using this directly, we are going to use it in Langchain Tool to add capability to the agent:" 232 | ] 233 | }, 234 | { 235 | "cell_type": "code", 236 | "execution_count": 3, 237 | "metadata": {}, 238 | "outputs": [], 239 | "source": [ 240 | "import vertexai\n", 241 | "vertexai.init(project=project_id, location=region, staging_bucket=staging_bucket_uri)\n", 242 | "\n", 243 | "from langchain_google_vertexai import VertexAIEmbeddings\n", 244 | "embeddings_service = VertexAIEmbeddings(model_name=gemini_embedding_model)\n", 245 | "\n", 246 | "from langchain_google_cloud_sql_pg import PostgresEngine\n", 247 | "\n", 248 | "pg_engine = PostgresEngine.from_instance(\n", 249 | " project_id=project_id,\n", 250 | " instance=instance_name,\n", 251 | " region=region,\n", 252 | " database=database_name,\n", 253 | " user=database_password,\n", 254 | " password=database_password,\n", 255 | ")\n", 256 | "\n", 257 | "from langchain_google_vertexai import VertexAIEmbeddings\n", 258 | "from langchain_google_cloud_sql_pg import PostgresVectorStore\n", 259 | "\n", 260 | "sample_vector_table_name = \"course_content_embeddings\"\n", 261 | "\n", 262 | "vector_store = PostgresVectorStore.create_sync(\n", 263 | " pg_engine,\n", 264 | " table_name=embeddings_table_name,\n", 265 | " embedding_service=embeddings_service,\n", 266 | " )\n", 267 | "retriever = vector_store.as_retriever(search_kwargs={\"k\": 10})" 268 | ] 269 | }, 270 | { 271 | "cell_type": "markdown", 272 | "metadata": {}, 273 | "source": [ 274 | "## Langchain Tool" 275 | ] 276 | }, 277 | { 278 | "cell_type": "markdown", 279 | "metadata": {}, 280 | "source": [ 281 | "### Search course tool with Postgres Vector Store\n", 282 | "\n", 283 | "This is the first tool that we will create. It is used to search content from the database given a user query.\n", 284 | "\n", 285 | "If you see internally, it only call `retriever.invoke()` and return the value. The other important thing is the description of the function. Thats how the agent knows when it needs to use this tool" 286 | ] 287 | }, 288 | { 289 | "cell_type": "code", 290 | "execution_count": 4, 291 | "metadata": {}, 292 | "outputs": [], 293 | "source": [ 294 | "@tool\n", 295 | "def search_course_content(query: str) -> str:\n", 296 | " \"\"\"Explain about software security course materials.\"\"\"\n", 297 | " result = str(retriever.invoke(query))\n", 298 | " return result" 299 | ] 300 | }, 301 | { 302 | "cell_type": "code", 303 | "execution_count": null, 304 | "metadata": {}, 305 | "outputs": [], 306 | "source": [ 307 | "search_course_content.invoke(\"best practices for forgot password\") " 308 | ] 309 | }, 310 | { 311 | "cell_type": "markdown", 312 | "metadata": {}, 313 | "source": [ 314 | "### Creating tool which calls API\n", 315 | "\n", 316 | "Now we define a simple python api client which will call the api we deployed earlier to cloud run." 317 | ] 318 | }, 319 | { 320 | "cell_type": "code", 321 | "execution_count": 6, 322 | "metadata": {}, 323 | "outputs": [], 324 | "source": [ 325 | "class CourseAPIClient:\n", 326 | " def __init__(self, url=api_base_url):\n", 327 | " self.url = url\n", 328 | " \n", 329 | " def list_courses(self):\n", 330 | " response = requests.get(f\"{self.url}/courses\")\n", 331 | " return response.json()\n", 332 | "\n", 333 | " def get_course(self, course_name):\n", 334 | " response = requests.get(f\"{self.url}/courses/{course_name}\")\n", 335 | " return response.json()\n", 336 | "\n", 337 | " def create_order(self, course, user_name, user_email):\n", 338 | " payload = {\n", 339 | " \"course\": course,\n", 340 | " \"user_name\": user_name,\n", 341 | " \"user_email\": user_email\n", 342 | " }\n", 343 | " response = requests.post(f\"{self.url}/orders\", json=payload)\n", 344 | " return response.json()\n", 345 | "\n", 346 | " def get_order(self, order_id):\n", 347 | " response = requests.get(f\"{self.url}/orders/{order_id}\")\n", 348 | " return response.json()\n", 349 | "\n", 350 | " def pay_order(self, order_id):\n", 351 | " response = requests.post(f\"{self.url}/orders/{order_id}:pay\")\n", 352 | " return response.json()\n", 353 | "\n", 354 | " def get_payment_page_url(self, order_id):\n", 355 | " return f\"{self.url}/orders/{order_id}/payment\"" 356 | ] 357 | }, 358 | { 359 | "cell_type": "markdown", 360 | "metadata": {}, 361 | "source": [ 362 | "For each api, we will define the tools here and call the relevant api function. Please note the description as well." 363 | ] 364 | }, 365 | { 366 | "cell_type": "code", 367 | "execution_count": 7, 368 | "metadata": {}, 369 | "outputs": [], 370 | "source": [ 371 | "from typing import List\n", 372 | "\n", 373 | "@tool\n", 374 | "def list_courses() -> List[str]:\n", 375 | " \"\"\"List all available courses sold on the platform.\"\"\"\n", 376 | " client = CourseAPIClient()\n", 377 | " return client.list_courses()" 378 | ] 379 | }, 380 | { 381 | "cell_type": "markdown", 382 | "metadata": {}, 383 | "source": [ 384 | "To help the agent decide what should be the input of the function, we can also define a input class and give proper description for the function and each input arguments" 385 | ] 386 | }, 387 | { 388 | "cell_type": "code", 389 | "execution_count": 8, 390 | "metadata": {}, 391 | "outputs": [], 392 | "source": [ 393 | "class GetCourseInput(BaseModel):\n", 394 | " course: str = Field(description=\"name of the course. this is the unique identifier of the course. it typically contains the course title with dashes, all in lowercase.\")\n", 395 | "\n", 396 | "@tool(\"get-course-tool\", args_schema=GetCourseInput)\n", 397 | "def get_course(course: str) -> str:\n", 398 | " \"\"\"Get course details by course name. course name is the unique identifier of the course. it typically contains the course title with dashes.\n", 399 | " This function can be used to get course details such as course price, etc.\"\"\"\n", 400 | " client = CourseAPIClient()\n", 401 | " return client.get_course(course)" 402 | ] 403 | }, 404 | { 405 | "cell_type": "markdown", 406 | "metadata": {}, 407 | "source": [ 408 | "Here you may use multiple arguments and perform some computation within the function/tools. In this case, the tool is used to create the order and return the order id and link to make the payment" 409 | ] 410 | }, 411 | { 412 | "cell_type": "code", 413 | "execution_count": 9, 414 | "metadata": {}, 415 | "outputs": [], 416 | "source": [ 417 | "class CreateOrderInput(BaseModel):\n", 418 | " course: str = Field(description=\"name of the course. this is the unique identifier of the course. it typically contains the course title with dashes, all in lowercase.\")\n", 419 | " user_name: str = Field(description=\"name of the user who is purchasing the course .\")\n", 420 | " user_email: str = Field(description=\"email of the user who is purchasing the course.\")\n", 421 | "\n", 422 | "@tool(\"create-order-tool\", args_schema=CreateOrderInput)\n", 423 | "def create_order(course: str, user_name: str, user_email: str) -> str:\n", 424 | " \"\"\"Create order for a course. This function can be used to create an order for a course. When this function returns successfully, it will return payment url to user to make payment. \"\"\"\n", 425 | " client = CourseAPIClient()\n", 426 | " \n", 427 | " print(f\"Creating order for course: {course}, user_name: {user_name}, user_email: {user_email}\")\n", 428 | " \n", 429 | " res = client.create_order(course, user_name, user_email)\n", 430 | " print(res)\n", 431 | " order_id = res[\"order_id\"]\n", 432 | " payment_url = f\"{api_base_url}/orders/{order_id}/payment\"\n", 433 | " return f\"Order number {order_id} created successfully. Payment URL: {payment_url}\"" 434 | ] 435 | }, 436 | { 437 | "cell_type": "code", 438 | "execution_count": null, 439 | "metadata": {}, 440 | "outputs": [], 441 | "source": [ 442 | "create_order.invoke({\"course\":\"software-security\", \"user_name\":\"John Doe\", \"user_email\":\"imre@gmail.com\"}) " 443 | ] 444 | }, 445 | { 446 | "cell_type": "code", 447 | "execution_count": 11, 448 | "metadata": {}, 449 | "outputs": [], 450 | "source": [ 451 | "class GetOrderInput(BaseModel):\n", 452 | " order_number: str = Field(description=\"order number identifier. this is a unique identifier in uuid format.\")\n", 453 | "\n", 454 | "@tool(\"get-order-tool\", args_schema=GetOrderInput)\n", 455 | "def get_order(order_number: str) -> str:\n", 456 | " \"\"\"Get order by using order number. This function can be used to get order details such as payment status to check whether the order has been paid or not. If user already paid the course, say thanks\"\"\"\n", 457 | " client = CourseAPIClient()\n", 458 | " return client.get_order(order_number)" 459 | ] 460 | }, 461 | { 462 | "cell_type": "markdown", 463 | "metadata": {}, 464 | "source": [ 465 | "Once we have the tools ready, we are going to put them into an array which will be used later" 466 | ] 467 | }, 468 | { 469 | "cell_type": "code", 470 | "execution_count": 12, 471 | "metadata": {}, 472 | "outputs": [], 473 | "source": [ 474 | "tools = [search_course_content, list_courses, get_course, create_order, get_order]" 475 | ] 476 | }, 477 | { 478 | "cell_type": "markdown", 479 | "metadata": {}, 480 | "source": [ 481 | "### Prompt\n", 482 | "\n", 483 | "This is prompt that we are going to use. On the prompt below, we defined few things:\n", 484 | "* System context. This is used to tell who the bot is and what it should and shouldn't do.\n", 485 | "* Adding chat history. This is used so that the agent can keep the conversation relevant and stays within the same context.\n", 486 | "* User query. This is query or question directly given by the user\n", 487 | "* Agent scratchpad. This is internal data used by the agent to decide which tools to use." 488 | ] 489 | }, 490 | { 491 | "cell_type": "code", 492 | "execution_count": 13, 493 | "metadata": {}, 494 | "outputs": [], 495 | "source": [ 496 | "prompt = {\n", 497 | " \"chat_history\": lambda x: x[\"history\"],\n", 498 | " \"input\": lambda x: x[\"input\"],\n", 499 | " \"agent_scratchpad\": (\n", 500 | " lambda x: format_to_openai_function_messages(x[\"intermediate_steps\"])\n", 501 | " ),\n", 502 | "} | ChatPromptTemplate(\n", 503 | " messages = [\n", 504 | " SystemMessagePromptTemplate.from_template(\"\"\"\n", 505 | " You are a bot assistant that sells online course about software security. You only use information provided from datastore or tools. You can provide the information that is relevant to the user's question or the summary of the content. If they ask about the content, you can give them more detail about the content. If the user seems interested, you may suggest the user to enroll in the course. \n", 506 | " \"\"\"),\n", 507 | " MessagesPlaceholder(variable_name=\"chat_history\", optional=True),\n", 508 | " HumanMessagePromptTemplate.from_template(\"Use tools to answer this questions: {input}\"),\n", 509 | " MessagesPlaceholder(variable_name=\"agent_scratchpad\"),\n", 510 | " ]\n", 511 | ")" 512 | ] 513 | }, 514 | { 515 | "cell_type": "markdown", 516 | "metadata": {}, 517 | "source": [ 518 | "### Message History\n", 519 | "\n", 520 | "To keep the conversation on context, we use chat history storage in PostgresSQL. This is used to store the conversation history between the user and the agent. This is used to keep the conversation relevant and to keep the context of the conversation." 521 | ] 522 | }, 523 | { 524 | "cell_type": "code", 525 | "execution_count": 31, 526 | "metadata": {}, 527 | "outputs": [], 528 | "source": [ 529 | "from langchain_google_cloud_sql_pg import PostgresChatMessageHistory\n", 530 | "\n", 531 | "def get_session_history(session_id: str) -> BaseChatMessageHistory:\n", 532 | " return PostgresChatMessageHistory.create_sync(\n", 533 | " pg_engine,\n", 534 | " table_name=chat_history_table_name,\n", 535 | " session_id=session_id,\n", 536 | " )" 537 | ] 538 | }, 539 | { 540 | "cell_type": "markdown", 541 | "metadata": {}, 542 | "source": [ 543 | "### Defining Agent\n", 544 | "\n", 545 | "This is where we define configuration for the agent. \n", 546 | "\n", 547 | "Here we defined:\n", 548 | "* Safety settings for Gemini\n", 549 | "* Model parameter (e.g. temperature and safety settings)\n", 550 | "* Agent creation where we add the tools, promopt, model, session history, etc\n" 551 | ] 552 | }, 553 | { 554 | "cell_type": "code", 555 | "execution_count": 32, 556 | "metadata": {}, 557 | "outputs": [], 558 | "source": [ 559 | "## Model safety settings\n", 560 | "safety_settings = {\n", 561 | " HarmCategory.HARM_CATEGORY_UNSPECIFIED: HarmBlockThreshold.BLOCK_ONLY_HIGH,\n", 562 | " HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: HarmBlockThreshold.BLOCK_ONLY_HIGH,\n", 563 | " HarmCategory.HARM_CATEGORY_HARASSMENT: HarmBlockThreshold.BLOCK_ONLY_HIGH,\n", 564 | " HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: HarmBlockThreshold.BLOCK_ONLY_HIGH,\n", 565 | " HarmCategory.HARM_CATEGORY_HATE_SPEECH: HarmBlockThreshold.BLOCK_ONLY_HIGH,\n", 566 | "}\n", 567 | "\n", 568 | "## Model parameters\n", 569 | "model_kwargs = {\n", 570 | " \"temperature\": 0.5,\n", 571 | " \"safety_settings\": safety_settings,\n", 572 | "}\n", 573 | "\n", 574 | "agent = reasoning_engines.LangchainAgent(\n", 575 | " model=gemini_llm_model,\n", 576 | " tools=tools,\n", 577 | " prompt=prompt, \n", 578 | " chat_history=get_session_history,\n", 579 | " agent_executor_kwargs={\n", 580 | " \"return_intermediate_steps\": True,\n", 581 | " },\n", 582 | " model_kwargs=model_kwargs,\n", 583 | " enable_tracing=True,\n", 584 | ")" 585 | ] 586 | }, 587 | { 588 | "cell_type": "markdown", 589 | "metadata": {}, 590 | "source": [ 591 | "### Testing the agent" 592 | ] 593 | }, 594 | { 595 | "cell_type": "code", 596 | "execution_count": null, 597 | "metadata": {}, 598 | "outputs": [], 599 | "source": [ 600 | "import uuid\n", 601 | "\n", 602 | "# Generate a UUID for the session ID\n", 603 | "session_id = str(uuid.uuid4())\n", 604 | "print(f\"Generated session ID: {session_id}\")" 605 | ] 606 | }, 607 | { 608 | "cell_type": "code", 609 | "execution_count": null, 610 | "metadata": {}, 611 | "outputs": [], 612 | "source": [ 613 | "response = agent.query(\n", 614 | " input=\"Can you please share what are being taught on this course?\",\n", 615 | " config={\"configurable\": {\"session_id\": session_id}},\n", 616 | ")\n", 617 | "display(Markdown(response[\"output\"]))" 618 | ] 619 | }, 620 | { 621 | "cell_type": "code", 622 | "execution_count": null, 623 | "metadata": {}, 624 | "outputs": [], 625 | "source": [ 626 | "response = agent.query(\n", 627 | " input=\"Does it teach about how to design a forgot password system securely?\",\n", 628 | " config={\"configurable\": {\"session_id\": session_id}},\n", 629 | ")\n", 630 | "display(Markdown(response[\"output\"]))" 631 | ] 632 | }, 633 | { 634 | "cell_type": "code", 635 | "execution_count": null, 636 | "metadata": {}, 637 | "outputs": [], 638 | "source": [ 639 | "response = agent.query(\n", 640 | " input=\"How much this course costs?\",\n", 641 | " config={\"configurable\": {\"session_id\": session_id}},\n", 642 | ")\n", 643 | "display(Markdown(response[\"output\"]))" 644 | ] 645 | }, 646 | { 647 | "cell_type": "code", 648 | "execution_count": null, 649 | "metadata": {}, 650 | "outputs": [], 651 | "source": [ 652 | "response = agent.query(\n", 653 | " input=\"Yes. I want to enroll. What information do you need?\",\n", 654 | " config={\"configurable\": {\"session_id\": session_id}},\n", 655 | ")\n", 656 | "display(Markdown(response[\"output\"]))" 657 | ] 658 | }, 659 | { 660 | "cell_type": "code", 661 | "execution_count": null, 662 | "metadata": {}, 663 | "outputs": [], 664 | "source": [ 665 | "response = agent.query(\n", 666 | " input=\"My name is Mulyono, and my email is fufufafa@gmail.com\",\n", 667 | " config={\"configurable\": {\"session_id\": session_id}},\n", 668 | ")\n", 669 | "display(Markdown(response[\"output\"]))" 670 | ] 671 | }, 672 | { 673 | "cell_type": "code", 674 | "execution_count": null, 675 | "metadata": {}, 676 | "outputs": [], 677 | "source": [ 678 | "response = agent.query(\n", 679 | " input=\"I have made the payment. Can you please check?\",\n", 680 | " config={\"configurable\": {\"session_id\": session_id}},\n", 681 | ")\n", 682 | "display(Markdown(response[\"output\"]))" 683 | ] 684 | } 685 | ], 686 | "metadata": { 687 | "kernelspec": { 688 | "display_name": ".venv", 689 | "language": "python", 690 | "name": "python3" 691 | }, 692 | "language_info": { 693 | "codemirror_mode": { 694 | "name": "ipython", 695 | "version": 3 696 | }, 697 | "file_extension": ".py", 698 | "mimetype": "text/x-python", 699 | "name": "python", 700 | "nbconvert_exporter": "python", 701 | "pygments_lexer": "ipython3", 702 | "version": "3.11.10" 703 | } 704 | }, 705 | "nbformat": 4, 706 | "nbformat_minor": 2 707 | } 708 | -------------------------------------------------------------------------------- /03-cleanup.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Clean up\n", 8 | "\n", 9 | "To avoid from unexpected charges, **I highly recommend to just destroy or delete the gcp project**. \n", 10 | "\n", 11 | "However, if you want to delete resources created from this workshop only, please follow the steps" 12 | ] 13 | }, 14 | { 15 | "cell_type": "markdown", 16 | "metadata": {}, 17 | "source": [ 18 | "Start with initializing the vertexai client" 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": null, 24 | "metadata": {}, 25 | "outputs": [], 26 | "source": [ 27 | "from dotenv import load_dotenv\n", 28 | "load_dotenv()" 29 | ] 30 | }, 31 | { 32 | "cell_type": "code", 33 | "execution_count": null, 34 | "metadata": {}, 35 | "outputs": [], 36 | "source": [ 37 | "project_id = \"imrenagi-devfest-2024\" # @param {type:\"string\"}\n", 38 | "region = \"us-central1\" \n", 39 | "\n", 40 | "!gcloud config set project {project_id} --quiet\n", 41 | "\n", 42 | "import vertexai\n", 43 | "vertexai.init(project=project_id, location=region)\n" 44 | ] 45 | }, 46 | { 47 | "cell_type": "markdown", 48 | "metadata": {}, 49 | "source": [ 50 | "## Delete all project\n", 51 | "\n", 52 | "You can just directly run delete project to clean up everything. Be careful!" 53 | ] 54 | }, 55 | { 56 | "cell_type": "code", 57 | "execution_count": null, 58 | "metadata": {}, 59 | "outputs": [], 60 | "source": [ 61 | "!gcloud projects delete $project_id --quiet" 62 | ] 63 | }, 64 | { 65 | "cell_type": "markdown", 66 | "metadata": {}, 67 | "source": [ 68 | "## Delete reasoninig engines\n", 69 | "\n", 70 | "List all of the reasoning engine" 71 | ] 72 | }, 73 | { 74 | "cell_type": "code", 75 | "execution_count": null, 76 | "metadata": {}, 77 | "outputs": [], 78 | "source": [ 79 | "from vertexai.preview import reasoning_engines\n", 80 | "\n", 81 | "reasoning_engines.ReasoningEngine.list()" 82 | ] 83 | }, 84 | { 85 | "cell_type": "markdown", 86 | "metadata": {}, 87 | "source": [ 88 | "For each reasoning engine you have, delete them one by one by replacing the reasoning engine id below:" 89 | ] 90 | }, 91 | { 92 | "cell_type": "code", 93 | "execution_count": null, 94 | "metadata": {}, 95 | "outputs": [], 96 | "source": [ 97 | "remote_agent = reasoning_engines.ReasoningEngine('projects/908311267620/locations/us-central1/reasoningEngines/1702026407611203584')\n", 98 | "\n", 99 | "remote_agent.delete()" 100 | ] 101 | }, 102 | { 103 | "cell_type": "markdown", 104 | "metadata": {}, 105 | "source": [ 106 | "## Delete Cloudrun" 107 | ] 108 | }, 109 | { 110 | "cell_type": "code", 111 | "execution_count": null, 112 | "metadata": {}, 113 | "outputs": [], 114 | "source": [ 115 | "#delete cloudrun\n", 116 | "!gcloud run services delete courses-api --platform managed --quiet --region us-central1\n", 117 | "#delete reasoning engine" 118 | ] 119 | }, 120 | { 121 | "cell_type": "markdown", 122 | "metadata": {}, 123 | "source": [ 124 | "## Delete CloudSQL" 125 | ] 126 | }, 127 | { 128 | "cell_type": "code", 129 | "execution_count": null, 130 | "metadata": {}, 131 | "outputs": [], 132 | "source": [ 133 | "#delete cloudsql\n", 134 | "!gcloud sql instances delete ai-agent-showcase --quiet\n" 135 | ] 136 | } 137 | ], 138 | "metadata": { 139 | "kernelspec": { 140 | "display_name": "Python 3", 141 | "language": "python", 142 | "name": "python3" 143 | }, 144 | "language_info": { 145 | "codemirror_mode": { 146 | "name": "ipython", 147 | "version": 3 148 | }, 149 | "file_extension": ".py", 150 | "mimetype": "text/x-python", 151 | "name": "python", 152 | "nbconvert_exporter": "python", 153 | "pygments_lexer": "ipython3", 154 | "version": "3.11.6" 155 | } 156 | }, 157 | "nbformat": 4, 158 | "nbformat_minor": 2 159 | } 160 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Building AI Agent Bot with RAG, Langchain and Reasoning Engine 2 | 3 | In this workshop, participants will embark on a comprehensive journey to build an AI agent using advanced tools and techniques such as Retrieval-Augmented Generation (RAG), Langchain, and Reasoning Engine. Over the course of 90 minutes, attendees will gain hands-on experience and valuable insights into the following key areas: 4 | 5 | 1. Preparing Documents for RAG: Learn how to prepare documents for Retrieval-Augmented Generation by embedding, chunking, and storing them in a vector database. We will utilize the pgvec extension in PostgreSQL to efficiently manage and query our document vectors. 6 | 7 | 1. Creating a Document Retriever Tool: Discover how to develop a powerful document retriever tool that performs efficient searches and retrievals from the vector database. This tool will be crucial for augmenting prompts with relevant information, enhancing the AI agent's responses. 8 | 9 | 1. Developing API Tools for Third-Party Interaction: Explore the process of creating API tools that enable the AI agent to interact seamlessly with third-party systems API. These tools will expand the agent's capabilities, allowing it to execute complex tasks and retrieve external data. 10 | 11 | 1. Building an Agent in Langchain: Dive into the creation of an intelligent agent using Langchain. Participants will learn how to manage chat histories through session stores (both in-memory and persistent storage like Redis) and leverage various tools to empower the agent to make decisions and perform actions autonomously. 12 | 13 | 1. Deploying the AI Agent to the Cloud with Reasoning Engine: Gain practical knowledge on deploying the AI agent to the cloud using the Reasoning Engine. This section will demonstrate how to transition from development to a production-ready prototype swiftly, ensuring the agent's scalability and reliability. 14 | 15 | By the end of this workshop, participants will have a robust understanding of building and deploying an AI agent, equipped with the skills to create intelligent systems that can autonomously interact with users and third-party services. This workshop will provide a comprehensive guideline, empowering attendees to innovate and implement AI solutions effectively in their own projects. 16 | 17 | --- 18 | 19 | In this workshop, we will build an AI agent bot designed to answer questions about an online course it promotes and sells. This intelligent agent will have a comprehensive understanding of the course content, enabling users to interact with it and ask detailed questions about the material. The agent will be capable of providing accurate and informative responses based on the data it has been trained on. 20 | 21 | Moreover, our AI agent will go beyond just answering questions. It will have the capability to accept orders, generate payment URLs, and check the status of orders, allowing users to make purchases directly through natural language interaction with the agent. This hands-on experience will demonstrate the agent's ability to perform complex tasks autonomously. 22 | 23 | Participants are encouraged to use any Large Language Models (LLMs) they are familiar with. However, for this workshop, I will be using Google Gemini 1.5 Pro. There will be $5 GCP credit that will be provided for the participants so that they can use Gemini model. For other LLM, they must provide their API Key as the credits will not be provided. 24 | 25 | Additionally, if participants have a Google Cloud Platform (GCP) account, they will be able to follow along with the deployment process, as we will be deploying the AI agent to the cloud using Google Cloud's Reasoning Engine. 26 | 27 | By the end of this workshop, participants will have hands-on experience and a clear understanding of how to build and deploy a robust AI agent capable of interacting with users and performing complex tasks, all using open-source software and cloud technologies. 28 | -------------------------------------------------------------------------------- /api/.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | go.work.sum 23 | 24 | # env file 25 | .env -------------------------------------------------------------------------------- /api/Dockerfile: -------------------------------------------------------------------------------- 1 | # Build stage 2 | FROM golang:1.23-alpine AS builder 3 | 4 | WORKDIR /app 5 | 6 | # Copy go mod and sum files 7 | COPY go.mod go.sum ./ 8 | 9 | # Download all dependencies 10 | RUN go mod download 11 | 12 | # Copy the source code 13 | COPY . . 14 | 15 | # Build the application 16 | RUN CGO_ENABLED=0 GOOS=linux go build -o main . 17 | 18 | # Final stage 19 | FROM alpine:latest 20 | 21 | WORKDIR /root/ 22 | 23 | # Copy the binary from builder 24 | COPY --from=builder /app/main . 25 | 26 | # Expose port 8080 27 | EXPOSE 8080 28 | 29 | # Command to run the executable 30 | CMD ["./main"] 31 | -------------------------------------------------------------------------------- /api/README.md: -------------------------------------------------------------------------------- 1 | 2 | docker build -t pycon-course-api:latest . 3 | 4 | docker run -p 8080:8080 -d -it --rm --name pycon-api pycon-course-api:latest 5 | 6 | docker stop pycon-api 7 | 8 | # Enable Artifact Registry API in your GCP project 9 | # Replace [PROJECT_ID] with your actual GCP project ID 10 | 11 | gcloud services enable artifactregistry.googleapis.com --project=[PROJECT_ID] 12 | 13 | 14 | -------------------------------------------------------------------------------- /api/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Get the first argument passed to the script and store it as project name 4 | if [ $# -eq 0 ]; then 5 | echo "Error: No project name provided. Please provide a project name as an argument." 6 | exit 1 7 | fi 8 | 9 | PROJECT_NAME="$1" 10 | 11 | # Get the second argument passed to the script and store it as region 12 | if [ $# -lt 2 ]; then 13 | echo "Error: No region provided. Please provide a project name and region as arguments." 14 | exit 1 15 | fi 16 | 17 | REGION="$2" 18 | 19 | # Set the project in gcloud 20 | gcloud config set project "$PROJECT_NAME" 21 | 22 | # Verify the project is set correctly 23 | echo "Deploying to project: $PROJECT_NAME" 24 | # Verify the region is set correctly 25 | echo "Deploying to region: $REGION" 26 | 27 | 28 | # Build the Docker image using Cloud Build 29 | gcloud builds submit --tag gcr.io/$PROJECT_NAME/courses-api . 30 | 31 | gcloud run deploy courses-api --allow-unauthenticated --region $REGION --quiet --image gcr.io/$PROJECT_NAME/courses-api 32 | -------------------------------------------------------------------------------- /api/go.mod: -------------------------------------------------------------------------------- 1 | module api 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/google/uuid v1.6.0 // indirect 7 | github.com/gorilla/mux v1.8.1 // indirect 8 | ) 9 | -------------------------------------------------------------------------------- /api/go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 2 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 3 | github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= 4 | github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= 5 | -------------------------------------------------------------------------------- /api/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "time" 9 | 10 | "github.com/google/uuid" 11 | "github.com/gorilla/mux" 12 | ) 13 | 14 | type Course struct { 15 | Name string `json:"name"` 16 | DisplayName string `json:"display_name"` 17 | Description string `json:"description"` 18 | Price float64 `json:"price"` 19 | Currency string `json:"currency"` 20 | } 21 | 22 | type Order struct { 23 | ID string `json:"id"` 24 | Course string `json:"course"` 25 | Price float64 `json:"price"` 26 | Currency string `json:"currency"` 27 | UserEmail string `json:"user_email"` 28 | UserName string `json:"user_name"` 29 | Status string `json:"status"` 30 | CreatedAt time.Time `json:"created_at"` 31 | PaidAt time.Time `json:"paid_at"` 32 | } 33 | 34 | var coursesDB = map[string]Course{ 35 | "software-security": { 36 | Name: "software-security", 37 | DisplayName: "Software Security", 38 | Description: "Learn how to secure your software", 39 | Price: 100.0, 40 | Currency: "USD", 41 | }, 42 | } 43 | 44 | var ordersDB = map[string]Order{} 45 | 46 | type Error struct { 47 | Code int `json:"code"` 48 | Message string `json:"message"` 49 | } 50 | 51 | func writeError(w http.ResponseWriter, code int, message string) { 52 | errorResponse := Error{ 53 | Message: message, 54 | Code: code, 55 | } 56 | jsonError, _ := json.Marshal(errorResponse) 57 | w.WriteHeader(code) 58 | w.Write(jsonError) 59 | } 60 | 61 | func ListCoursesHandler(w http.ResponseWriter, r *http.Request) { 62 | w.Header().Set("Content-Type", "application/json") 63 | 64 | courses := make([]Course, 0, len(coursesDB)) 65 | for _, course := range coursesDB { 66 | courses = append(courses, course) 67 | } 68 | 69 | jsonResponse, err := json.Marshal(courses) 70 | if err != nil { 71 | writeError(w, http.StatusInternalServerError, "Error encoding JSON") 72 | return 73 | } 74 | 75 | w.WriteHeader(http.StatusOK) 76 | w.Write(jsonResponse) 77 | } 78 | 79 | func GetCourseHandler(w http.ResponseWriter, r *http.Request) { 80 | w.Header().Set("Content-Type", "application/json") 81 | 82 | vars := mux.Vars(r) 83 | courseName := vars["course"] 84 | 85 | course, ok := coursesDB[courseName] 86 | if !ok { 87 | writeError(w, http.StatusNotFound, "Course not found") 88 | return 89 | } 90 | 91 | jsonResponse, err := json.Marshal(course) 92 | if err != nil { 93 | writeError(w, http.StatusInternalServerError, "Error encoding JSON") 94 | return 95 | } 96 | 97 | w.WriteHeader(http.StatusOK) 98 | w.Write(jsonResponse) 99 | } 100 | 101 | func CreateOrderHandler(w http.ResponseWriter, r *http.Request) { 102 | w.Header().Set("Content-Type", "application/json") 103 | 104 | // Define Order struct 105 | type CreateOrderRequest struct { 106 | Course string `json:"course"` 107 | UserName string `json:"user_name"` 108 | UserEmail string `json:"user_email"` 109 | } 110 | 111 | // Parse request body 112 | var newOrder CreateOrderRequest 113 | err := json.NewDecoder(r.Body).Decode(&newOrder) 114 | if err != nil { 115 | writeError(w, http.StatusBadRequest, "Invalid request body") 116 | return 117 | } 118 | 119 | // Check if the course is valid 120 | course, ok := coursesDB[newOrder.Course] 121 | if !ok { 122 | writeError(w, http.StatusNotFound, "Course not found") 123 | return 124 | } 125 | 126 | order := Order{ 127 | ID: uuid.New().String(), 128 | Course: newOrder.Course, 129 | UserName: newOrder.UserName, 130 | UserEmail: newOrder.UserEmail, 131 | Status: "pending", 132 | CreatedAt: time.Now(), 133 | Price: course.Price, 134 | Currency: course.Currency, 135 | } 136 | 137 | // Add new order to the map (assuming ordersDB is defined elsewhere) 138 | ordersDB[order.ID] = order 139 | 140 | // Create response with payment page URL 141 | type CreateOrderResponse struct { 142 | OrderID string `json:"order_id"` 143 | } 144 | 145 | response := CreateOrderResponse{ 146 | OrderID: order.ID, 147 | } 148 | 149 | // Send response 150 | jsonResponse, err := json.Marshal(response) 151 | if err != nil { 152 | writeError(w, http.StatusInternalServerError, "Error encoding JSON") 153 | return 154 | } 155 | 156 | w.WriteHeader(http.StatusCreated) 157 | w.Write(jsonResponse) 158 | } 159 | 160 | func GetOrderHandler(w http.ResponseWriter, r *http.Request) { 161 | w.Header().Set("Content-Type", "application/json") 162 | 163 | vars := mux.Vars(r) 164 | orderID := vars["order"] 165 | 166 | // Get order from database 167 | order, ok := ordersDB[orderID] 168 | if !ok { 169 | writeError(w, http.StatusNotFound, "Order not found") 170 | return 171 | } 172 | 173 | // Prepare JSON response 174 | jsonResponse, err := json.Marshal(order) 175 | if err != nil { 176 | writeError(w, http.StatusInternalServerError, "Error encoding JSON") 177 | return 178 | } 179 | w.WriteHeader(http.StatusOK) 180 | w.Write(jsonResponse) 181 | } 182 | 183 | func PayOrderHandler(w http.ResponseWriter, r *http.Request) { 184 | w.Header().Set("Content-Type", "application/json") 185 | 186 | vars := mux.Vars(r) 187 | orderID := vars["order"] 188 | 189 | // Get order from database 190 | order, ok := ordersDB[orderID] 191 | if !ok { 192 | writeError(w, http.StatusNotFound, "Order not found") 193 | return 194 | } 195 | 196 | // Update order status 197 | order.Status = "paid" 198 | order.PaidAt = time.Now() 199 | 200 | // Update order in database 201 | ordersDB[orderID] = order 202 | 203 | // Prepare JSON response 204 | jsonResponse, err := json.Marshal(order) 205 | if err != nil { 206 | writeError(w, http.StatusInternalServerError, "Error encoding JSON") 207 | return 208 | } 209 | w.WriteHeader(http.StatusOK) 210 | w.Write(jsonResponse) 211 | } 212 | 213 | func OrderPaymentPageHandler(w http.ResponseWriter, r *http.Request) { 214 | vars := mux.Vars(r) 215 | orderID := vars["order"] 216 | 217 | // Get order from database 218 | order, ok := ordersDB[orderID] 219 | if !ok { 220 | http.Error(w, "Order not found", http.StatusNotFound) 221 | return 222 | } 223 | 224 | // Create HTML content 225 | html := fmt.Sprintf(` 226 | 227 | 228 | 229 | 230 | 231 | Pay for Order %s 232 | 233 | 234 |

Pay for Order %s

235 |

Total Amount: $%.2f

236 |
237 | 238 |
239 | 240 | 241 | `, orderID, orderID, order.Price, orderID) 242 | 243 | // Set content type and write HTML 244 | w.Header().Set("Content-Type", "text/html") 245 | w.WriteHeader(http.StatusOK) 246 | w.Write([]byte(html)) 247 | } 248 | 249 | func main() { 250 | r := mux.NewRouter() 251 | 252 | r.HandleFunc("/courses", ListCoursesHandler).Methods("GET") 253 | r.HandleFunc("/courses/{course}", GetCourseHandler).Methods("GET") 254 | r.HandleFunc("/orders", CreateOrderHandler).Methods("POST") 255 | r.HandleFunc("/orders/{order}", GetOrderHandler).Methods("GET") 256 | r.HandleFunc("/orders/{order}/payment", OrderPaymentPageHandler).Methods("GET") 257 | r.HandleFunc("/orders/{order}:pay", PayOrderHandler).Methods("POST") 258 | 259 | http.Handle("/", r) 260 | 261 | fmt.Println("Server is starting on port 8080...") 262 | if err := http.ListenAndServe(":8080", nil); err != nil { 263 | log.Fatal("ListenAndServe error: ", err) 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | postgres: 3 | image: pgvector/pgvector:pg16 4 | environment: 5 | POSTGRES_DB: pyconapac 6 | POSTGRES_USER: pyconapac 7 | POSTGRES_PASSWORD: pyconapac 8 | ports: 9 | - "5432:5432" 10 | restart: always 11 | volumes: 12 | - postgres:/var/lib/postgresql/data/ 13 | 14 | prometheus: 15 | image: prom/prometheus:latest 16 | volumes: 17 | - ./scripts/prometheus:/etc/prometheus 18 | - prometheus_data:/prometheus 19 | ports: 20 | - "9090:9090" 21 | command: 22 | - '--config.file=/etc/prometheus/prometheus.yml' 23 | - '--storage.tsdb.retention.time=5m' 24 | - '--storage.tsdb.retention.size=10GB' 25 | 26 | grafana: 27 | image: grafana/grafana:latest 28 | ports: 29 | - 3000:3000 30 | volumes: 31 | - ./scripts/grafana/provisioning:/etc/grafana/provisioning 32 | - ./scripts/grafana/dashboards:/etc/grafana/demo-dashboards 33 | - grafana_data:/var/lib/grafana 34 | 35 | jaeger: 36 | image: jaegertracing/all-in-one:latest 37 | environment: 38 | COLLECTOR_ZIPKIN_HOST_PORT: 9411 39 | METRICS_STORAGE_TYPE: prometheus 40 | LOG_LEVEL: debug 41 | expose: 42 | - "16686" 43 | ports: 44 | - 5775:5775/udp 45 | - 6831:6831/udp 46 | - 6832:6832/udp 47 | - 5778:5778 48 | - 16686:16686 49 | - 14268:14268 50 | - 14250:14250 51 | - 14269:14269 52 | - 9411:9411 53 | command: 54 | - "--memory.max-traces" 55 | - "1000" 56 | - "--prometheus.server-url" 57 | - "http://prometheus:9090" 58 | - "--collector.otlp.grpc.host-port" 59 | - ":4317" 60 | - "--collector.otlp.http.host-port" 61 | - ":4318" 62 | # - "--prometheus.query.support-spanmetrics-connector" 63 | # - "true" 64 | restart: always 65 | 66 | otel_collector: 67 | image: otel/opentelemetry-collector-contrib:latest 68 | expose: 69 | - "4317" 70 | - "4318" 71 | ports: 72 | - "1888:1888" # pprof extension 73 | - "8888:8888" # Prometheus metrics exposed by the collector 74 | - "8889:8889" # Prometheus exporter metrics 75 | - "13133:13133" # health_check extension 76 | - "4317:4317" # OTLP gRPC receiver 77 | - "4318:4318" # OTLP HTTP receiver 78 | - "55679:55679" # zpages extension 79 | volumes: 80 | - "./scripts/opentelemetry:/observability" 81 | command: ["--config=/observability/config.yaml"] 82 | restart: always 83 | depends_on: [jaeger] 84 | 85 | volumes: 86 | postgres: 87 | prometheus_data: 88 | grafana_data: 89 | -------------------------------------------------------------------------------- /experiment/02-embedding.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "# dependency imports\n", 10 | "from langchain_google_vertexai import VertexAIEmbeddings\n", 11 | "\n", 12 | "from langchain_community.document_loaders import UnstructuredMarkdownLoader\n", 13 | "from langchain.text_splitter import MarkdownTextSplitter\n", 14 | "\n", 15 | "from langchain_core.documents import Document\n", 16 | "\n", 17 | "import pandas as pd" 18 | ] 19 | }, 20 | { 21 | "cell_type": "code", 22 | "execution_count": 2, 23 | "metadata": {}, 24 | "outputs": [], 25 | "source": [ 26 | "# Set this to true this if you want to use cloudsql\n", 27 | "# USE_CLOUDSQL = False\n", 28 | "USE_CLOUDSQL = True\n", 29 | "\n", 30 | "project_id = \"imrenagi-gemini-experiment\" #change this to your project id\n", 31 | "region = \"us-central1\"\n", 32 | "gemini_embedding_model = \"text-embedding-004\"\n", 33 | "\n", 34 | "if not USE_CLOUDSQL:\n", 35 | " # use pgvector docker image for local development\n", 36 | " database_password = \"pyconapac\"\n", 37 | " database_name = \"pyconapac\"\n", 38 | " database_user = \"pyconapac\"\n", 39 | " database_host = \"localhost\"\n", 40 | "else:\n", 41 | " # use cloudsql credential if you want to use cloudsql\n", 42 | " instance_name=\"pyconapac-demo\"\n", 43 | " database_password = 'testing'\n", 44 | " database_name = 'testing'\n", 45 | " database_user = 'testing'\n", 46 | "\n", 47 | "assert database_name, \"⚠️ Please provide a database name\"\n", 48 | "assert database_user, \"⚠️ Please provide a database user\"\n", 49 | "assert database_password, \"⚠️ Please provide a database password\"\n" 50 | ] 51 | }, 52 | { 53 | "cell_type": "code", 54 | "execution_count": null, 55 | "metadata": {}, 56 | "outputs": [], 57 | "source": [ 58 | "#@markdown ###Authenticate your Google Cloud Account and enable APIs.\n", 59 | "# Authenticate gcloud.\n", 60 | "# from google.colab import auth\n", 61 | "# auth.authenticate_user()\n", 62 | "\n", 63 | "# Configure gcloud.\n", 64 | "!gcloud config set project {project_id}\n", 65 | "\n", 66 | "# Grant Cloud SQL Client role to authenticated user\n", 67 | "current_user = !gcloud auth list --filter=status:ACTIVE --format=\"value(account)\"\n", 68 | "print(f\"{current_user}\")\n", 69 | "# enable aiplatform apiservices" 70 | ] 71 | }, 72 | { 73 | "cell_type": "code", 74 | "execution_count": null, 75 | "metadata": {}, 76 | "outputs": [], 77 | "source": [ 78 | "if USE_CLOUDSQL:\n", 79 | " print(f\"Granting Cloud SQL Client role to {current_user[0]}\")\n", 80 | " # granting cloudsql client role to the current user\n", 81 | " !gcloud projects add-iam-policy-binding {project_id} \\\n", 82 | " --member=user:{current_user[0]} \\\n", 83 | " --role=\"roles/cloudsql.client\"\n", 84 | " # Enable Cloud SQL Admin API\n", 85 | " !gcloud services enable sqladmin.googleapis.com" 86 | ] 87 | }, 88 | { 89 | "cell_type": "code", 90 | "execution_count": null, 91 | "metadata": {}, 92 | "outputs": [], 93 | "source": [ 94 | "if USE_CLOUDSQL:\n", 95 | " #@markdown Create and setup a Cloud SQL PostgreSQL instance, if not done already.\n", 96 | " database_version = !gcloud sql instances describe {instance_name} --format=\"value(databaseVersion)\"\n", 97 | " if database_version[0].startswith(\"POSTGRES\"):\n", 98 | " print(\"Found an existing Postgres Cloud SQL Instance!\")\n", 99 | " else:\n", 100 | " print(\"Creating new Cloud SQL instance...\")\n", 101 | " !gcloud sql instances create {instance_name} --database-version=POSTGRES_15 \\\n", 102 | " --region={region} --cpu=1 --memory=4GB --root-password={database_password} \\\n", 103 | " --authorized-networks=0.0.0.0/0\n", 104 | " # Create the database, if it does not exist.\n", 105 | " out = !gcloud sql databases list --instance={instance_name} --filter=\"NAME:{database_name}\" --format=\"value(NAME)\"\n", 106 | " if ''.join(out) == database_name:\n", 107 | " print(\"Database %s already exists, skipping creation.\" % database_name)\n", 108 | " else:\n", 109 | " !gcloud sql databases create {database_name} --instance={instance_name}\n", 110 | " # Create the database user for accessing the database.\n", 111 | " !gcloud sql users create {database_user} \\\n", 112 | " --instance={instance_name} \\\n", 113 | " --password={database_password}" 114 | ] 115 | }, 116 | { 117 | "cell_type": "code", 118 | "execution_count": null, 119 | "metadata": {}, 120 | "outputs": [], 121 | "source": [ 122 | "if USE_CLOUDSQL:\n", 123 | " # get the ip address of the instance\n", 124 | " ip_addresses = !gcloud sql instances describe {instance_name} --project {project_id} --format 'value(ipAddresses.ipAddress)'\n", 125 | " # Split the IP addresses and take the first one\n", 126 | " database_host = ip_addresses[0].split(';')[0].strip()\n", 127 | " print(f\"Using database host: {database_host}\")" 128 | ] 129 | }, 130 | { 131 | "cell_type": "code", 132 | "execution_count": null, 133 | "metadata": {}, 134 | "outputs": [], 135 | "source": [ 136 | "db_conn_string = f\"postgres://{database_user}:{database_password}@{database_host}:5432/{database_name}\"\n", 137 | "db_conn_string" 138 | ] 139 | }, 140 | { 141 | "cell_type": "code", 142 | "execution_count": null, 143 | "metadata": {}, 144 | "outputs": [], 145 | "source": [ 146 | "# Read the JSONL file into a pandas DataFrame\n", 147 | "df = pd.read_json('course_content.jsonl', lines=True)\n", 148 | "df.head(5)" 149 | ] 150 | }, 151 | { 152 | "cell_type": "code", 153 | "execution_count": 9, 154 | "metadata": {}, 155 | "outputs": [], 156 | "source": [ 157 | "import asyncpg\n", 158 | "\n", 159 | "async def main():\n", 160 | " # Create connection to PostgreSQL database\n", 161 | " conn = await asyncpg.connect(\n", 162 | " host=database_host,\n", 163 | " user=database_user,\n", 164 | " password=database_password,\n", 165 | " database=database_name\n", 166 | " )\n", 167 | "\n", 168 | " try:\n", 169 | " await conn.execute(\"DROP TABLE IF EXISTS course_contents CASCADE\")\n", 170 | " # Create the `course_contents` table.\n", 171 | " await conn.execute(\n", 172 | " \"\"\"CREATE TABLE IF NOT EXISTS course_contents (\n", 173 | " id SERIAL PRIMARY KEY,\n", 174 | " title TEXT,\n", 175 | " content TEXT,\n", 176 | " file_path TEXT,\n", 177 | " slug TEXT\n", 178 | " )\"\"\"\n", 179 | " )\n", 180 | "\n", 181 | " # Create an index on the slug column for faster lookups\n", 182 | " await conn.execute(\n", 183 | " \"\"\"CREATE INDEX IF NOT EXISTS idx_course_contents_slug \n", 184 | " ON course_contents (slug)\"\"\"\n", 185 | " )\n", 186 | "\n", 187 | " # Copy the dataframe to the `course_contents` table.\n", 188 | " tuples = list(df.itertuples(index=False))\n", 189 | " await conn.copy_records_to_table(\n", 190 | " \"course_contents\", records=tuples, columns=list(df), timeout=10\n", 191 | " )\n", 192 | " finally:\n", 193 | " await conn.close()\n", 194 | "\n", 195 | "# Run the SQL commands now.\n", 196 | "await main() # type: ignore" 197 | ] 198 | }, 199 | { 200 | "cell_type": "code", 201 | "execution_count": null, 202 | "metadata": {}, 203 | "outputs": [], 204 | "source": [ 205 | "from langchain.text_splitter import MarkdownTextSplitter\n", 206 | "\n", 207 | "text_splitter = MarkdownTextSplitter(\n", 208 | " chunk_size=1000, \n", 209 | " chunk_overlap=200)\n", 210 | "\n", 211 | "chunked = []\n", 212 | "for index, row in df.iterrows():\n", 213 | " course_content_id = row[\"id\"]\n", 214 | " title = row[\"title\"]\n", 215 | " content = row[\"content\"]\n", 216 | " splits = text_splitter.create_documents([content])\n", 217 | " for s in splits:\n", 218 | " r = {\"course_content_id\": course_content_id, \"content\": s.page_content}\n", 219 | " chunked.append(r)\n", 220 | "\n", 221 | "chunked_df = pd.DataFrame(chunked)\n", 222 | "chunked_df.head(5)" 223 | ] 224 | }, 225 | { 226 | "cell_type": "code", 227 | "execution_count": 11, 228 | "metadata": {}, 229 | "outputs": [], 230 | "source": [ 231 | "from langchain_google_vertexai import VertexAIEmbeddings\n", 232 | "import time\n", 233 | "import vertexai\n", 234 | "\n", 235 | "# Initialize Vertex AI\n", 236 | "vertexai.init(project=project_id, location=region)\n", 237 | "# Create a Vertex AI Embeddings service\n", 238 | "embeddings_service = VertexAIEmbeddings(model_name=gemini_embedding_model)" 239 | ] 240 | }, 241 | { 242 | "cell_type": "code", 243 | "execution_count": 12, 244 | "metadata": {}, 245 | "outputs": [], 246 | "source": [ 247 | "# Helper function to retry failed API requests with exponential backoff.\n", 248 | "def retry_with_backoff(func, *args, retry_delay=5, backoff_factor=2, **kwargs):\n", 249 | " max_attempts = 10\n", 250 | " retries = 0\n", 251 | " for i in range(max_attempts):\n", 252 | " try:\n", 253 | " return func(*args, **kwargs)\n", 254 | " except Exception as e:\n", 255 | " print(f\"error: {e}\")\n", 256 | " retries += 1\n", 257 | " wait = retry_delay * (backoff_factor**retries)\n", 258 | " print(f\"Retry after waiting for {wait} seconds...\")\n", 259 | " time.sleep(wait)\n", 260 | "\n", 261 | "\n", 262 | "batch_size = 5\n", 263 | "for i in range(0, len(chunked), batch_size):\n", 264 | " request = [x[\"content\"] for x in chunked[i : i + batch_size]]\n", 265 | " response = retry_with_backoff(embeddings_service.embed_documents, request)\n", 266 | " # Store the retrieved vector embeddings for each chunk back.\n", 267 | " for x, e in zip(chunked[i : i + batch_size], response):\n", 268 | " x[\"embedding\"] = e" 269 | ] 270 | }, 271 | { 272 | "cell_type": "code", 273 | "execution_count": null, 274 | "metadata": {}, 275 | "outputs": [], 276 | "source": [ 277 | "# Store the generated embeddings in a pandas dataframe.\n", 278 | "course_content_embeddings = pd.DataFrame(chunked)\n", 279 | "course_content_embeddings.head()" 280 | ] 281 | }, 282 | { 283 | "cell_type": "code", 284 | "execution_count": 14, 285 | "metadata": {}, 286 | "outputs": [], 287 | "source": [ 288 | "# Store the generated vector embeddings in a PostgreSQL table.\n", 289 | "# This code may run for a few minutes.\n", 290 | "import numpy as np\n", 291 | "import asyncpg\n", 292 | "from pgvector.asyncpg import register_vector\n", 293 | "\n", 294 | "async def main():\n", 295 | " conn = await asyncpg.connect(\n", 296 | " host=database_host,\n", 297 | " user=database_user,\n", 298 | " password=database_password,\n", 299 | " database=database_name\n", 300 | " )\n", 301 | "\n", 302 | " # this is not used since we already use pgvector docker container\n", 303 | " await conn.execute(\"CREATE EXTENSION IF NOT EXISTS vector\")\n", 304 | " await register_vector(conn)\n", 305 | "\n", 306 | " await conn.execute(\"DROP TABLE IF EXISTS course_content_embeddings\")\n", 307 | " # Create the `product_embeddings` table to store vector embeddings.\n", 308 | " await conn.execute(\n", 309 | " \"\"\"CREATE TABLE IF NOT EXISTS course_content_embeddings(\n", 310 | " id INTEGER NOT NULL REFERENCES course_contents(id),\n", 311 | " content TEXT,\n", 312 | " embedding vector(768))\"\"\"\n", 313 | " )\n", 314 | "\n", 315 | " # Store all the generated embeddings back into the database.\n", 316 | " for index, row in course_content_embeddings.iterrows():\n", 317 | " await conn.execute(\n", 318 | " \"INSERT INTO course_content_embeddings (id, content, embedding) VALUES ($1, $2, $3)\",\n", 319 | " row[\"course_content_id\"],\n", 320 | " row[\"content\"],\n", 321 | " np.array(row[\"embedding\"]),\n", 322 | " )\n", 323 | "\n", 324 | " await conn.close()\n", 325 | "\n", 326 | "# Run the SQL commands now.\n", 327 | "await main() # type: ignore" 328 | ] 329 | }, 330 | { 331 | "cell_type": "markdown", 332 | "metadata": {}, 333 | "source": [ 334 | "### Create indexes for faster similarity search in pgvector\n", 335 | "\n", 336 | "- Vector indexes can significantly speed up similarity search operation and avoid the brute-force exact nearest neighbor search that is used by default.\n", 337 | "\n", 338 | "- pgvector comes with two types of indexes (as of v0.5.1): `hnsw` and `ivfflat`.\n", 339 | "\n", 340 | "> 💡 Click [here](https://cloud.google.com/blog/products/databases/faster-similarity-search-performance-with-pgvector-indexes) to learn more about pgvector indexes.\n", 341 | "\n", 342 | "Enter or modify the values of index parameters for your index of choice and run the corresponding cell:" 343 | ] 344 | }, 345 | { 346 | "cell_type": "code", 347 | "execution_count": 15, 348 | "metadata": {}, 349 | "outputs": [], 350 | "source": [ 351 | "# @markdown Create an HNSW index on the `course_content_embeddings` table:\n", 352 | "m = 24 # @param {type:\"integer\"}\n", 353 | "ef_construction = 100 # @param {type:\"integer\"}\n", 354 | "operator = \"vector_cosine_ops\" # @param [\"vector_cosine_ops\", \"vector_l2_ops\", \"vector_ip_ops\"]\n", 355 | "\n", 356 | "# Quick input validations.\n", 357 | "assert m, \"⚠️ Please input a valid value for m.\"\n", 358 | "assert ef_construction, \"⚠️ Please input a valid value for ef_construction.\"\n", 359 | "assert operator, \"⚠️ Please input a valid value for operator.\"\n", 360 | "\n", 361 | "import asyncpg\n", 362 | "from pgvector.asyncpg import register_vector\n", 363 | "\n", 364 | "async def main():\n", 365 | " conn = await asyncpg.connect(\n", 366 | " host=database_host,\n", 367 | " user=database_user,\n", 368 | " password=database_password,\n", 369 | " database=database_name\n", 370 | " )\n", 371 | " await register_vector(conn)\n", 372 | "\n", 373 | " # Create an HNSW index on the `course_content_embeddings` table.\n", 374 | " await conn.execute(\n", 375 | " f\"\"\"CREATE INDEX ON course_content_embeddings\n", 376 | " USING hnsw(embedding {operator})\n", 377 | " WITH (m = {m}, ef_construction = {ef_construction})\n", 378 | " \"\"\"\n", 379 | " )\n", 380 | " await conn.close()\n", 381 | "\n", 382 | "\n", 383 | "# Run the SQL commands now.\n", 384 | "await main() # type: ignore" 385 | ] 386 | }, 387 | { 388 | "cell_type": "code", 389 | "execution_count": 16, 390 | "metadata": {}, 391 | "outputs": [], 392 | "source": [ 393 | "query = \"what is the best way to design forgot password\" # @param {type:\"string\"}\n", 394 | "\n", 395 | "assert query, \"⚠️ Please input a valid input search text\"\n", 396 | "\n", 397 | "qe = embeddings_service.embed_query(query)" 398 | ] 399 | }, 400 | { 401 | "cell_type": "code", 402 | "execution_count": null, 403 | "metadata": {}, 404 | "outputs": [], 405 | "source": [ 406 | "# Convert the query embedding to a numpy array to inspect the content\n", 407 | "np.array(qe)" 408 | ] 409 | }, 410 | { 411 | "cell_type": "code", 412 | "execution_count": null, 413 | "metadata": {}, 414 | "outputs": [], 415 | "source": [ 416 | "from pgvector.asyncpg import register_vector\n", 417 | "import asyncpg\n", 418 | "\n", 419 | "matches = []\n", 420 | "\n", 421 | "async def main():\n", 422 | " conn = await asyncpg.connect(\n", 423 | " host=database_host,\n", 424 | " user=database_user,\n", 425 | " password=database_password,\n", 426 | " database=database_name\n", 427 | " )\n", 428 | " await register_vector(conn)\n", 429 | " \n", 430 | " similarity_threshold = 0.1\n", 431 | " num_matches = 50\n", 432 | "\n", 433 | " results = await conn.fetch(\n", 434 | " \"\"\"\n", 435 | " WITH vector_matches AS (\n", 436 | " SELECT id, content, 1 - (embedding <=> $1) AS similarity\n", 437 | " FROM course_content_embeddings\n", 438 | " WHERE 1 - (embedding <=> $1) > $2\n", 439 | " ORDER BY similarity DESC\n", 440 | " LIMIT $3\n", 441 | " )\n", 442 | " SELECT cc.id as id, cc.title as title, \n", 443 | " vm.content as content, \n", 444 | " vm.similarity as similarity \n", 445 | " FROM course_contents cc\n", 446 | " LEFT JOIN vector_matches vm ON cc.id = vm.id;\n", 447 | " \"\"\",\n", 448 | " qe,\n", 449 | " similarity_threshold,\n", 450 | " num_matches,\n", 451 | " \n", 452 | " )\n", 453 | "\n", 454 | " if len(results) == 0:\n", 455 | " raise Exception(\"Did not find any results. Adjust the query parameters.\")\n", 456 | "\n", 457 | " for r in results:\n", 458 | " # Collect the description for all the matched similar contents.\n", 459 | " matches.append(\n", 460 | " {\n", 461 | " \"id\": r[\"id\"],\n", 462 | " \"title\": r[\"title\"],\n", 463 | " \"content\": r[\"content\"],\n", 464 | " \"similarity\": r[\"similarity\"], \n", 465 | " }\n", 466 | " )\n", 467 | "\n", 468 | " await conn.close()\n", 469 | "\n", 470 | "\n", 471 | "# Run the SQL commands now.\n", 472 | "await main() # type: ignore\n", 473 | "\n", 474 | "matches = pd.DataFrame(matches)\n", 475 | "matches.head(10)" 476 | ] 477 | }, 478 | { 479 | "cell_type": "markdown", 480 | "metadata": {}, 481 | "source": [ 482 | "# Cleanup" 483 | ] 484 | }, 485 | { 486 | "cell_type": "code", 487 | "execution_count": 19, 488 | "metadata": {}, 489 | "outputs": [], 490 | "source": [ 491 | "# if USE_CLOUDSQL:\n", 492 | "# !gcloud sql instances delete {instance_name} --quiet" 493 | ] 494 | } 495 | ], 496 | "metadata": { 497 | "kernelspec": { 498 | "display_name": ".venv", 499 | "language": "python", 500 | "name": "python3" 501 | }, 502 | "language_info": { 503 | "codemirror_mode": { 504 | "name": "ipython", 505 | "version": 3 506 | }, 507 | "file_extension": ".py", 508 | "mimetype": "text/x-python", 509 | "name": "python", 510 | "nbconvert_exporter": "python", 511 | "pygments_lexer": "ipython3", 512 | "version": "3.11.10" 513 | } 514 | }, 515 | "nbformat": 4, 516 | "nbformat_minor": 2 517 | } 518 | -------------------------------------------------------------------------------- /experiment/03-retriever.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "# USE_CLOUDSQL = False\n", 10 | "USE_CLOUDSQL = True\n", 11 | "\n", 12 | "\n", 13 | "project_id = \"imrenagi-gemini-experiment\" #change this to your project id\n", 14 | "region = \"us-central1\" \n", 15 | "gemini_embedding_model = \"text-embedding-004\"\n", 16 | "\n", 17 | "if not USE_CLOUDSQL:\n", 18 | " # use pgvector docker image for local development\n", 19 | " database_password = \"pyconapac\"\n", 20 | " database_name = \"pyconapac\"\n", 21 | " database_user = \"pyconapac\"\n", 22 | " database_host = \"localhost\"\n", 23 | "else:\n", 24 | " # use cloudsql credential if you want to use cloudsql\n", 25 | " instance_name=\"pyconapac-demo\"\n", 26 | " database_password = 'testing'\n", 27 | " database_name = 'testing'\n", 28 | " database_user = 'testing'\n", 29 | "\n", 30 | "\n", 31 | "assert database_name, \"⚠️ Please provide a database name\"\n", 32 | "assert database_user, \"⚠️ Please provide a database user\"\n", 33 | "assert database_password, \"⚠️ Please provide a database password\"" 34 | ] 35 | }, 36 | { 37 | "cell_type": "code", 38 | "execution_count": 2, 39 | "metadata": {}, 40 | "outputs": [], 41 | "source": [ 42 | "if USE_CLOUDSQL:\n", 43 | " # get the ip address of the cloudsql instance\n", 44 | " ip_addresses = !gcloud sql instances describe {instance_name} --format=\"value(ipAddresses[0].ipAddress)\"\n", 45 | " database_host = ip_addresses[0]" 46 | ] 47 | }, 48 | { 49 | "cell_type": "code", 50 | "execution_count": null, 51 | "metadata": {}, 52 | "outputs": [], 53 | "source": [ 54 | "db_conn_string = f\"postgres://{database_user}:{database_password}@{database_host}:5432/{database_name}\"\n", 55 | "db_conn_string" 56 | ] 57 | }, 58 | { 59 | "cell_type": "code", 60 | "execution_count": 4, 61 | "metadata": {}, 62 | "outputs": [], 63 | "source": [ 64 | "import vertexai\n", 65 | "vertexai.init(project=project_id, location=region)\n", 66 | "\n", 67 | "from langchain_google_vertexai import VertexAIEmbeddings\n", 68 | "embeddings_service = VertexAIEmbeddings(model_name=gemini_embedding_model)" 69 | ] 70 | }, 71 | { 72 | "cell_type": "code", 73 | "execution_count": null, 74 | "metadata": {}, 75 | "outputs": [], 76 | "source": [ 77 | "%%writefile lib/pg_retriever.py\n", 78 | "\n", 79 | "from typing import List\n", 80 | "\n", 81 | "from langchain_core.callbacks import CallbackManagerForRetrieverRun\n", 82 | "from langchain_core.documents import Document\n", 83 | "from langchain_core.retrievers import BaseRetriever\n", 84 | "\n", 85 | "from langchain_google_vertexai import VertexAIEmbeddings\n", 86 | "\n", 87 | "import psycopg2\n", 88 | "from pgvector.psycopg2 import register_vector\n", 89 | "\n", 90 | "class CourseContentRetriever(BaseRetriever):\n", 91 | " \"\"\"Retriever to find relevant course content based on the\n", 92 | " query provided.\"\"\"\n", 93 | "\n", 94 | " embeddings_service: VertexAIEmbeddings \n", 95 | " similarity_threshold: float\n", 96 | " num_matches: int\n", 97 | " conn_str: str\n", 98 | "\n", 99 | " def _get_relevant_documents(\n", 100 | " self, query: str, *, run_manager: CallbackManagerForRetrieverRun\n", 101 | " ) -> List[Document]:\n", 102 | " conn = psycopg2.connect(self.conn_str)\n", 103 | " register_vector(conn)\n", 104 | "\n", 105 | " qe = self.embeddings_service.embed_query(query)\n", 106 | "\n", 107 | " with conn.cursor() as cur:\n", 108 | " cur.execute(\n", 109 | " \"\"\"\n", 110 | " WITH vector_matches AS (\n", 111 | " SELECT id, content, 1 - (embedding <=> %s::vector) AS similarity\n", 112 | " FROM course_content_embeddings\n", 113 | " WHERE 1 - (embedding <=> %s::vector) > %s\n", 114 | " ORDER BY similarity DESC\n", 115 | " LIMIT %s\n", 116 | " )\n", 117 | " SELECT cc.id as id, cc.title as title, \n", 118 | " vm.content as content, \n", 119 | " vm.similarity as similarity \n", 120 | " FROM course_contents cc\n", 121 | " LEFT JOIN vector_matches vm ON cc.id = vm.id;\n", 122 | " \"\"\",\n", 123 | " (qe, qe, self.similarity_threshold, self.num_matches)\n", 124 | " )\n", 125 | " results = cur.fetchall()\n", 126 | "\n", 127 | " conn.close()\n", 128 | "\n", 129 | " if not results:\n", 130 | " return []\n", 131 | " \n", 132 | " return [\n", 133 | " Document(\n", 134 | " page_content=r[2],\n", 135 | " metadata={\n", 136 | " \"id\": r[0],\n", 137 | " \"title\": r[1],\n", 138 | " \"similarity\": r[3],\n", 139 | " }\n", 140 | " ) for r in results if r[2] is not None\n", 141 | " ]" 142 | ] 143 | }, 144 | { 145 | "cell_type": "code", 146 | "execution_count": null, 147 | "metadata": {}, 148 | "outputs": [], 149 | "source": [ 150 | "from lib.pg_retriever import CourseContentRetriever\n", 151 | "\n", 152 | "retriever = CourseContentRetriever(embeddings_service=embeddings_service, conn_str=db_conn_string, similarity_threshold=0.1, num_matches=10)\n", 153 | "retriever.invoke(\"what is strategy for creating forgot password\", run_manager=None)" 154 | ] 155 | }, 156 | { 157 | "cell_type": "code", 158 | "execution_count": 9, 159 | "metadata": {}, 160 | "outputs": [], 161 | "source": [ 162 | "from typing import List\n", 163 | "\n", 164 | "from langchain_core.callbacks import CallbackManagerForRetrieverRun\n", 165 | "from langchain_core.documents import Document\n", 166 | "from langchain_core.retrievers import BaseRetriever\n", 167 | "\n", 168 | "from langchain_google_vertexai import VertexAIEmbeddings\n", 169 | "\n", 170 | "import asyncpg\n", 171 | "import asyncio\n", 172 | "from pgvector.asyncpg import register_vector\n", 173 | "\n", 174 | "class CourseContentRetriever(BaseRetriever):\n", 175 | " \"\"\"Retriever to find relevant course content based on the\n", 176 | " query provided.\"\"\"\n", 177 | "\n", 178 | " embeddings_service: VertexAIEmbeddings \n", 179 | " similarity_threshold: float\n", 180 | " num_matches: int\n", 181 | " conn_str: str\n", 182 | "\n", 183 | " async def _aget_relevant_documents(\n", 184 | " self, query: str, *, run_manager: CallbackManagerForRetrieverRun\n", 185 | " ) -> List[Document]:\n", 186 | " conn = await asyncpg.connect(self.conn_str)\n", 187 | " await register_vector(conn)\n", 188 | "\n", 189 | " qe = await self.embeddings_service.aembed_query(query)\n", 190 | "\n", 191 | " results = await conn.fetch(\n", 192 | " \"\"\"\n", 193 | " WITH vector_matches AS (\n", 194 | " SELECT id, content, 1 - (embedding <=> $1::vector) AS similarity\n", 195 | " FROM course_content_embeddings\n", 196 | " WHERE 1 - (embedding <=> $1::vector) > $2\n", 197 | " ORDER BY similarity DESC\n", 198 | " LIMIT $3\n", 199 | " )\n", 200 | " SELECT cc.id as id, cc.title as title, \n", 201 | " vm.content as content, \n", 202 | " vm.similarity as similarity \n", 203 | " FROM course_contents cc\n", 204 | " LEFT JOIN vector_matches vm ON cc.id = vm.id;\n", 205 | " \"\"\",\n", 206 | " qe, self.similarity_threshold, self.num_matches\n", 207 | " )\n", 208 | "\n", 209 | " await conn.close()\n", 210 | "\n", 211 | " if not results:\n", 212 | " return []\n", 213 | " \n", 214 | " return [\n", 215 | " Document(\n", 216 | " page_content=r['content'],\n", 217 | " metadata={\n", 218 | " \"id\": r['id'],\n", 219 | " \"title\": r['title'],\n", 220 | " \"similarity\": r['similarity'],\n", 221 | " }\n", 222 | " ) for r in results if r['content'] is not None\n", 223 | " ]\n", 224 | "\n", 225 | " def _get_relevant_documents(\n", 226 | " self, query: str, *, run_manager: CallbackManagerForRetrieverRun\n", 227 | " ) -> List[Document]:\n", 228 | " return asyncio.run(self._aget_relevant_documents(query, run_manager=run_manager))" 229 | ] 230 | }, 231 | { 232 | "cell_type": "code", 233 | "execution_count": null, 234 | "metadata": {}, 235 | "outputs": [], 236 | "source": [ 237 | "async def test():\n", 238 | " retriever = CourseContentRetriever(embeddings_service=embeddings_service, conn_str=db_conn_string, similarity_threshold=0.1, num_matches=10)\n", 239 | " retriever.invoke(\"what is strategy for creating forgot password\", run_manager=None)\n", 240 | "\n", 241 | "await test()" 242 | ] 243 | } 244 | ], 245 | "metadata": { 246 | "kernelspec": { 247 | "display_name": ".venv", 248 | "language": "python", 249 | "name": "python3" 250 | }, 251 | "language_info": { 252 | "codemirror_mode": { 253 | "name": "ipython", 254 | "version": 3 255 | }, 256 | "file_extension": ".py", 257 | "mimetype": "text/x-python", 258 | "name": "python", 259 | "nbconvert_exporter": "python", 260 | "pygments_lexer": "ipython3", 261 | "version": "3.11.10" 262 | } 263 | }, 264 | "nbformat": 4, 265 | "nbformat_minor": 2 266 | } 267 | -------------------------------------------------------------------------------- /experiment/openllmmetry.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Simple AI Agent" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": null, 13 | "metadata": {}, 14 | "outputs": [], 15 | "source": [ 16 | "from dotenv import load_dotenv\n", 17 | "load_dotenv()" 18 | ] 19 | }, 20 | { 21 | "cell_type": "code", 22 | "execution_count": null, 23 | "metadata": {}, 24 | "outputs": [], 25 | "source": [ 26 | "from traceloop.sdk import Traceloop\n", 27 | "Traceloop.init(app_name=\"testing-openllmetry\")" 28 | ] 29 | }, 30 | { 31 | "cell_type": "markdown", 32 | "metadata": {}, 33 | "source": [ 34 | "## Setting up the tools\n", 35 | "\n", 36 | "This AI use web based document loader to load pages from langsmit documentation. " 37 | ] 38 | }, 39 | { 40 | "cell_type": "code", 41 | "execution_count": null, 42 | "metadata": {}, 43 | "outputs": [], 44 | "source": [ 45 | "from langchain_community.document_loaders import WebBaseLoader\n", 46 | "from langchain_community.vectorstores import FAISS\n", 47 | "from langchain_openai import OpenAIEmbeddings\n", 48 | "from langchain_text_splitters import RecursiveCharacterTextSplitter\n", 49 | "from IPython.display import display, Markdown\n", 50 | "\n", 51 | "loader = WebBaseLoader(\"https://docs.smith.langchain.com/overview\")\n", 52 | "docs = loader.load()\n", 53 | "documents = RecursiveCharacterTextSplitter(\n", 54 | " chunk_size=1000, chunk_overlap=200\n", 55 | ").split_documents(docs)\n", 56 | "vector = FAISS.from_documents(documents, OpenAIEmbeddings())\n", 57 | "retriever = vector.as_retriever()" 58 | ] 59 | }, 60 | { 61 | "cell_type": "code", 62 | "execution_count": null, 63 | "metadata": {}, 64 | "outputs": [], 65 | "source": [ 66 | "retriever.invoke(\"What is Langsmith?\")" 67 | ] 68 | }, 69 | { 70 | "cell_type": "code", 71 | "execution_count": 5, 72 | "metadata": {}, 73 | "outputs": [], 74 | "source": [ 75 | "from langchain.tools.retriever import create_retriever_tool\n", 76 | "\n", 77 | "retriever_tool = create_retriever_tool(\n", 78 | " retriever,\n", 79 | " \"langsmith_search\",\n", 80 | " \"Search for information about LangSmith. For any questions about LangSmith, you must use this tool!\",\n", 81 | ")\n", 82 | "\n", 83 | "tools = [retriever_tool]" 84 | ] 85 | }, 86 | { 87 | "cell_type": "markdown", 88 | "metadata": {}, 89 | "source": [ 90 | "## Setting up the models and agent" 91 | ] 92 | }, 93 | { 94 | "cell_type": "code", 95 | "execution_count": 6, 96 | "metadata": {}, 97 | "outputs": [], 98 | "source": [ 99 | "from langchain_google_vertexai import ChatVertexAI\n", 100 | "\n", 101 | "model = ChatVertexAI(model=\"gemini-1.5-flash\")\n", 102 | "\n", 103 | "from langchain import hub\n", 104 | "prompt = hub.pull(\"hwchase17/openai-functions-agent\")\n", 105 | "\n", 106 | "from langchain.agents import AgentExecutor\n", 107 | "from langchain.agents import create_tool_calling_agent\n", 108 | "\n", 109 | "agent = create_tool_calling_agent(model, tools, prompt)\n", 110 | "agent_executor = AgentExecutor(agent=agent, tools=tools)" 111 | ] 112 | }, 113 | { 114 | "cell_type": "markdown", 115 | "metadata": {}, 116 | "source": [ 117 | "## Add session storage" 118 | ] 119 | }, 120 | { 121 | "cell_type": "code", 122 | "execution_count": 7, 123 | "metadata": {}, 124 | "outputs": [], 125 | "source": [ 126 | "\n", 127 | "from langchain_community.chat_message_histories import ChatMessageHistory\n", 128 | "from langchain_core.chat_history import BaseChatMessageHistory\n", 129 | "from langchain_core.runnables.history import RunnableWithMessageHistory\n", 130 | "\n", 131 | "store = {}\n", 132 | "\n", 133 | "def get_session_history(session_id: str) -> BaseChatMessageHistory:\n", 134 | " if session_id not in store:\n", 135 | " store[session_id] = ChatMessageHistory()\n", 136 | " return store[session_id]" 137 | ] 138 | }, 139 | { 140 | "cell_type": "code", 141 | "execution_count": 8, 142 | "metadata": {}, 143 | "outputs": [], 144 | "source": [ 145 | "agent_with_chat_history = RunnableWithMessageHistory(\n", 146 | " agent_executor,\n", 147 | " get_session_history,\n", 148 | " input_messages_key=\"input\",\n", 149 | " history_messages_key=\"chat_history\",\n", 150 | ")" 151 | ] 152 | }, 153 | { 154 | "cell_type": "code", 155 | "execution_count": 9, 156 | "metadata": {}, 157 | "outputs": [], 158 | "source": [ 159 | "import uuid\n", 160 | "\n", 161 | "session_id = str(uuid.uuid4())" 162 | ] 163 | }, 164 | { 165 | "cell_type": "code", 166 | "execution_count": null, 167 | "metadata": {}, 168 | "outputs": [], 169 | "source": [ 170 | "response = agent_with_chat_history.invoke(\n", 171 | " {\"input\": \"How langsmith can help with testing the LLM agent?\"},\n", 172 | " config={\"configurable\": {\"session_id\": session_id}},\n", 173 | ")\n", 174 | "\n", 175 | "display(Markdown(response[\"output\"]))\n" 176 | ] 177 | } 178 | ], 179 | "metadata": { 180 | "kernelspec": { 181 | "display_name": "Python 3", 182 | "language": "python", 183 | "name": "python3" 184 | }, 185 | "language_info": { 186 | "codemirror_mode": { 187 | "name": "ipython", 188 | "version": 3 189 | }, 190 | "file_extension": ".py", 191 | "mimetype": "text/x-python", 192 | "name": "python", 193 | "nbconvert_exporter": "python", 194 | "pygments_lexer": "ipython3", 195 | "version": "3.11.10" 196 | } 197 | }, 198 | "nbformat": 4, 199 | "nbformat_minor": 2 200 | } 201 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | 2 | google-cloud-aiplatform 3 | google-cloud-aiplatform[langchain] 4 | google-cloud-aiplatform[reasoningengine] 5 | langchain 6 | langchain_core 7 | langchain_community 8 | langchain-google-vertexai==2.0.8 9 | cloudpickle 10 | pydantic==2.9.2 11 | langchain-google-community 12 | google-cloud-discoveryengine 13 | nest-asyncio 14 | asyncio==3.4.3 15 | asyncpg==0.29.0 16 | cloud-sql-python-connector[asyncpg] 17 | langchain-google-cloud-sql-pg 18 | numpy 19 | pandas 20 | pgvector 21 | psycopg2-binary 22 | langchain-openai 23 | langgraph 24 | traceloop-sdk 25 | opentelemetry-instrumentation-google-generativeai 26 | opentelemetry-instrumentation-langchain 27 | opentelemetry-instrumentation-vertexai 28 | python-dotenv 29 | -------------------------------------------------------------------------------- /scripts/grafana/dashboards/dashboard.json: -------------------------------------------------------------------------------- 1 | { 2 | "annotations": { 3 | "list": [ 4 | { 5 | "builtIn": 1, 6 | "datasource": { 7 | "type": "grafana", 8 | "uid": "-- Grafana --" 9 | }, 10 | "enable": true, 11 | "hide": true, 12 | "iconColor": "rgba(0, 211, 255, 1)", 13 | "name": "Annotations & Alerts", 14 | "type": "dashboard" 15 | } 16 | ] 17 | }, 18 | "editable": true, 19 | "fiscalYearStartMonth": 0, 20 | "graphTooltip": 0, 21 | "id": 2, 22 | "links": [], 23 | "liveNow": false, 24 | "panels": [ 25 | { 26 | "datasource": { 27 | "type": "prometheus", 28 | "uid": "PBFA97CFB590B2093" 29 | }, 30 | "fieldConfig": { 31 | "defaults": { 32 | "color": { 33 | "mode": "palette-classic" 34 | }, 35 | "custom": { 36 | "axisBorderShow": false, 37 | "axisCenteredZero": false, 38 | "axisColorMode": "text", 39 | "axisLabel": "", 40 | "axisPlacement": "auto", 41 | "barAlignment": 0, 42 | "drawStyle": "line", 43 | "fillOpacity": 0, 44 | "gradientMode": "none", 45 | "hideFrom": { 46 | "legend": false, 47 | "tooltip": false, 48 | "viz": false 49 | }, 50 | "insertNulls": false, 51 | "lineInterpolation": "linear", 52 | "lineWidth": 1, 53 | "pointSize": 5, 54 | "scaleDistribution": { 55 | "type": "linear" 56 | }, 57 | "showPoints": "auto", 58 | "spanNulls": false, 59 | "stacking": { 60 | "group": "A", 61 | "mode": "none" 62 | }, 63 | "thresholdsStyle": { 64 | "mode": "off" 65 | } 66 | }, 67 | "mappings": [], 68 | "thresholds": { 69 | "mode": "absolute", 70 | "steps": [ 71 | { 72 | "color": "green", 73 | "value": null 74 | }, 75 | { 76 | "color": "red", 77 | "value": 80 78 | } 79 | ] 80 | }, 81 | "unit": "s" 82 | }, 83 | "overrides": [] 84 | }, 85 | "gridPos": { 86 | "h": 9, 87 | "w": 7, 88 | "x": 0, 89 | "y": 0 90 | }, 91 | "id": 3, 92 | "options": { 93 | "legend": { 94 | "calcs": [], 95 | "displayMode": "list", 96 | "placement": "bottom", 97 | "showLegend": true 98 | }, 99 | "tooltip": { 100 | "mode": "single", 101 | "sort": "none" 102 | } 103 | }, 104 | "targets": [ 105 | { 106 | "datasource": { 107 | "type": "prometheus", 108 | "uid": "PBFA97CFB590B2093" 109 | }, 110 | "editorMode": "code", 111 | "expr": "histogram_quantile(0.95, sum(rate(gen_ai_client_operation_duration_seconds_bucket{\n }[$__rate_interval])) \nby (le, gen_ai_response_model))", 112 | "instant": false, 113 | "legendFormat": "{{gen_ai_response_model}}", 114 | "range": true, 115 | "refId": "A" 116 | } 117 | ], 118 | "title": "P95 Gen AI Client Operation ", 119 | "type": "timeseries" 120 | }, 121 | { 122 | "datasource": { 123 | "type": "prometheus", 124 | "uid": "PBFA97CFB590B2093" 125 | }, 126 | "fieldConfig": { 127 | "defaults": { 128 | "color": { 129 | "mode": "palette-classic" 130 | }, 131 | "custom": { 132 | "axisBorderShow": false, 133 | "axisCenteredZero": false, 134 | "axisColorMode": "text", 135 | "axisLabel": "", 136 | "axisPlacement": "auto", 137 | "barAlignment": 0, 138 | "drawStyle": "line", 139 | "fillOpacity": 0, 140 | "gradientMode": "none", 141 | "hideFrom": { 142 | "legend": false, 143 | "tooltip": false, 144 | "viz": false 145 | }, 146 | "insertNulls": false, 147 | "lineInterpolation": "linear", 148 | "lineWidth": 1, 149 | "pointSize": 5, 150 | "scaleDistribution": { 151 | "type": "linear" 152 | }, 153 | "showPoints": "auto", 154 | "spanNulls": false, 155 | "stacking": { 156 | "group": "A", 157 | "mode": "none" 158 | }, 159 | "thresholdsStyle": { 160 | "mode": "off" 161 | } 162 | }, 163 | "mappings": [], 164 | "thresholds": { 165 | "mode": "absolute", 166 | "steps": [ 167 | { 168 | "color": "green", 169 | "value": null 170 | }, 171 | { 172 | "color": "red", 173 | "value": 80 174 | } 175 | ] 176 | }, 177 | "unit": "none" 178 | }, 179 | "overrides": [] 180 | }, 181 | "gridPos": { 182 | "h": 9, 183 | "w": 9, 184 | "x": 7, 185 | "y": 0 186 | }, 187 | "id": 4, 188 | "options": { 189 | "legend": { 190 | "calcs": [], 191 | "displayMode": "list", 192 | "placement": "bottom", 193 | "showLegend": true 194 | }, 195 | "tooltip": { 196 | "mode": "single", 197 | "sort": "none" 198 | } 199 | }, 200 | "targets": [ 201 | { 202 | "datasource": { 203 | "type": "prometheus", 204 | "uid": "PBFA97CFB590B2093" 205 | }, 206 | "editorMode": "code", 207 | "expr": "histogram_quantile(0.95, sum(rate(gen_ai_client_token_usage_bucket{\n }[$__rate_interval])) \nby (le, gen_ai_response_model))", 208 | "instant": false, 209 | "legendFormat": "{{gen_ai_response_model}}", 210 | "range": true, 211 | "refId": "A" 212 | } 213 | ], 214 | "title": "P95 Gen AI Client Token Usage ", 215 | "type": "timeseries" 216 | }, 217 | { 218 | "datasource": { 219 | "type": "prometheus", 220 | "uid": "PBFA97CFB590B2093" 221 | }, 222 | "fieldConfig": { 223 | "defaults": { 224 | "color": { 225 | "mode": "thresholds" 226 | }, 227 | "mappings": [], 228 | "thresholds": { 229 | "mode": "absolute", 230 | "steps": [ 231 | { 232 | "color": "green", 233 | "value": null 234 | } 235 | ] 236 | } 237 | }, 238 | "overrides": [] 239 | }, 240 | "gridPos": { 241 | "h": 4, 242 | "w": 4, 243 | "x": 16, 244 | "y": 0 245 | }, 246 | "id": 5, 247 | "options": { 248 | "colorMode": "value", 249 | "graphMode": "none", 250 | "justifyMode": "auto", 251 | "orientation": "auto", 252 | "reduceOptions": { 253 | "calcs": [ 254 | "lastNotNull" 255 | ], 256 | "fields": "", 257 | "values": false 258 | }, 259 | "textMode": "auto", 260 | "wideLayout": true 261 | }, 262 | "pluginVersion": "10.2.2", 263 | "targets": [ 264 | { 265 | "datasource": { 266 | "type": "prometheus", 267 | "uid": "PBFA97CFB590B2093" 268 | }, 269 | "editorMode": "code", 270 | "expr": "sum(increase(gen_ai_client_token_usage_sum{exported_job=\"testing-openllmetry\"}[1h]))", 271 | "instant": false, 272 | "legendFormat": "__auto", 273 | "range": true, 274 | "refId": "A" 275 | } 276 | ], 277 | "title": "Total Token Usage Past 1h", 278 | "type": "stat" 279 | }, 280 | { 281 | "datasource": { 282 | "type": "prometheus", 283 | "uid": "PBFA97CFB590B2093" 284 | }, 285 | "fieldConfig": { 286 | "defaults": { 287 | "color": { 288 | "mode": "thresholds" 289 | }, 290 | "mappings": [], 291 | "thresholds": { 292 | "mode": "absolute", 293 | "steps": [ 294 | { 295 | "color": "green", 296 | "value": null 297 | } 298 | ] 299 | } 300 | }, 301 | "overrides": [] 302 | }, 303 | "gridPos": { 304 | "h": 4, 305 | "w": 4, 306 | "x": 20, 307 | "y": 0 308 | }, 309 | "id": 8, 310 | "options": { 311 | "colorMode": "value", 312 | "graphMode": "none", 313 | "justifyMode": "auto", 314 | "orientation": "auto", 315 | "reduceOptions": { 316 | "calcs": [ 317 | "lastNotNull" 318 | ], 319 | "fields": "", 320 | "values": false 321 | }, 322 | "textMode": "auto", 323 | "wideLayout": true 324 | }, 325 | "pluginVersion": "10.2.2", 326 | "targets": [ 327 | { 328 | "datasource": { 329 | "type": "prometheus", 330 | "uid": "PBFA97CFB590B2093" 331 | }, 332 | "editorMode": "code", 333 | "expr": "sum(increase(gen_ai_client_token_usage_sum{exported_job=\"testing-openllmetry\"}[10m]))", 334 | "instant": false, 335 | "legendFormat": "__auto", 336 | "range": true, 337 | "refId": "A" 338 | } 339 | ], 340 | "title": "Total Token Usage Past 10m", 341 | "type": "stat" 342 | }, 343 | { 344 | "datasource": { 345 | "type": "prometheus", 346 | "uid": "PBFA97CFB590B2093" 347 | }, 348 | "fieldConfig": { 349 | "defaults": { 350 | "color": { 351 | "mode": "palette-classic" 352 | }, 353 | "custom": { 354 | "axisBorderShow": false, 355 | "axisCenteredZero": false, 356 | "axisColorMode": "text", 357 | "axisLabel": "", 358 | "axisPlacement": "auto", 359 | "barAlignment": 0, 360 | "drawStyle": "line", 361 | "fillOpacity": 0, 362 | "gradientMode": "none", 363 | "hideFrom": { 364 | "legend": false, 365 | "tooltip": false, 366 | "viz": false 367 | }, 368 | "insertNulls": false, 369 | "lineInterpolation": "linear", 370 | "lineWidth": 1, 371 | "pointSize": 5, 372 | "scaleDistribution": { 373 | "type": "linear" 374 | }, 375 | "showPoints": "auto", 376 | "spanNulls": false, 377 | "stacking": { 378 | "group": "A", 379 | "mode": "none" 380 | }, 381 | "thresholdsStyle": { 382 | "mode": "off" 383 | } 384 | }, 385 | "mappings": [], 386 | "thresholds": { 387 | "mode": "absolute", 388 | "steps": [ 389 | { 390 | "color": "green", 391 | "value": null 392 | }, 393 | { 394 | "color": "red", 395 | "value": 80 396 | } 397 | ] 398 | } 399 | }, 400 | "overrides": [] 401 | }, 402 | "gridPos": { 403 | "h": 5, 404 | "w": 8, 405 | "x": 16, 406 | "y": 4 407 | }, 408 | "id": 6, 409 | "options": { 410 | "legend": { 411 | "calcs": [], 412 | "displayMode": "list", 413 | "placement": "bottom", 414 | "showLegend": true 415 | }, 416 | "tooltip": { 417 | "mode": "single", 418 | "sort": "none" 419 | } 420 | }, 421 | "targets": [ 422 | { 423 | "datasource": { 424 | "type": "prometheus", 425 | "uid": "PBFA97CFB590B2093" 426 | }, 427 | "editorMode": "code", 428 | "expr": "sum(increase(gen_ai_client_token_usage_sum[10m]))", 429 | "instant": false, 430 | "legendFormat": "__auto", 431 | "range": true, 432 | "refId": "A" 433 | } 434 | ], 435 | "title": "Total Token Usage Past 10m", 436 | "type": "timeseries" 437 | }, 438 | { 439 | "datasource": { 440 | "type": "prometheus", 441 | "uid": "PBFA97CFB590B2093" 442 | }, 443 | "fieldConfig": { 444 | "defaults": { 445 | "color": { 446 | "mode": "palette-classic" 447 | }, 448 | "custom": { 449 | "axisBorderShow": false, 450 | "axisCenteredZero": false, 451 | "axisColorMode": "text", 452 | "axisLabel": "", 453 | "axisPlacement": "auto", 454 | "barAlignment": 0, 455 | "drawStyle": "line", 456 | "fillOpacity": 0, 457 | "gradientMode": "none", 458 | "hideFrom": { 459 | "legend": false, 460 | "tooltip": false, 461 | "viz": false 462 | }, 463 | "insertNulls": false, 464 | "lineInterpolation": "linear", 465 | "lineWidth": 1, 466 | "pointSize": 5, 467 | "scaleDistribution": { 468 | "type": "linear" 469 | }, 470 | "showPoints": "auto", 471 | "spanNulls": false, 472 | "stacking": { 473 | "group": "A", 474 | "mode": "none" 475 | }, 476 | "thresholdsStyle": { 477 | "mode": "off" 478 | } 479 | }, 480 | "mappings": [], 481 | "thresholds": { 482 | "mode": "absolute", 483 | "steps": [ 484 | { 485 | "color": "green", 486 | "value": null 487 | }, 488 | { 489 | "color": "red", 490 | "value": 80 491 | } 492 | ] 493 | }, 494 | "unit": "ms" 495 | }, 496 | "overrides": [] 497 | }, 498 | "gridPos": { 499 | "h": 11, 500 | "w": 12, 501 | "x": 0, 502 | "y": 9 503 | }, 504 | "id": 2, 505 | "options": { 506 | "legend": { 507 | "calcs": [], 508 | "displayMode": "table", 509 | "placement": "right", 510 | "showLegend": true 511 | }, 512 | "tooltip": { 513 | "mode": "single", 514 | "sort": "none" 515 | } 516 | }, 517 | "targets": [ 518 | { 519 | "datasource": { 520 | "type": "prometheus", 521 | "uid": "PBFA97CFB590B2093" 522 | }, 523 | "editorMode": "code", 524 | "expr": "histogram_quantile(0.95, sum(rate(duration_milliseconds_bucket{}[$__rate_interval])) by (span_name, le))", 525 | "instant": false, 526 | "legendFormat": "{{span_name}}", 527 | "range": true, 528 | "refId": "A" 529 | } 530 | ], 531 | "title": "Latency [ms]", 532 | "type": "timeseries" 533 | }, 534 | { 535 | "datasource": { 536 | "type": "prometheus", 537 | "uid": "PBFA97CFB590B2093" 538 | }, 539 | "fieldConfig": { 540 | "defaults": { 541 | "color": { 542 | "mode": "palette-classic" 543 | }, 544 | "custom": { 545 | "axisBorderShow": false, 546 | "axisCenteredZero": false, 547 | "axisColorMode": "text", 548 | "axisLabel": "", 549 | "axisPlacement": "auto", 550 | "barAlignment": 0, 551 | "drawStyle": "line", 552 | "fillOpacity": 0, 553 | "gradientMode": "none", 554 | "hideFrom": { 555 | "legend": false, 556 | "tooltip": false, 557 | "viz": false 558 | }, 559 | "insertNulls": false, 560 | "lineInterpolation": "linear", 561 | "lineWidth": 1, 562 | "pointSize": 5, 563 | "scaleDistribution": { 564 | "type": "linear" 565 | }, 566 | "showPoints": "auto", 567 | "spanNulls": false, 568 | "stacking": { 569 | "group": "A", 570 | "mode": "none" 571 | }, 572 | "thresholdsStyle": { 573 | "mode": "off" 574 | } 575 | }, 576 | "mappings": [], 577 | "thresholds": { 578 | "mode": "absolute", 579 | "steps": [ 580 | { 581 | "color": "green", 582 | "value": null 583 | }, 584 | { 585 | "color": "red", 586 | "value": 80 587 | } 588 | ] 589 | } 590 | }, 591 | "overrides": [] 592 | }, 593 | "gridPos": { 594 | "h": 11, 595 | "w": 12, 596 | "x": 12, 597 | "y": 9 598 | }, 599 | "id": 1, 600 | "options": { 601 | "legend": { 602 | "calcs": [], 603 | "displayMode": "table", 604 | "placement": "right", 605 | "showLegend": true 606 | }, 607 | "tooltip": { 608 | "mode": "single", 609 | "sort": "none" 610 | } 611 | }, 612 | "targets": [ 613 | { 614 | "datasource": { 615 | "type": "prometheus", 616 | "uid": "PBFA97CFB590B2093" 617 | }, 618 | "editorMode": "code", 619 | "expr": "rate(calls_total{\n service_name=\"testing-openllmetry\",\n }[$__interval])", 620 | "instant": false, 621 | "legendFormat": "{{span_name}}", 622 | "range": true, 623 | "refId": "A" 624 | } 625 | ], 626 | "title": "Total Calls", 627 | "type": "timeseries" 628 | } 629 | ], 630 | "refresh": false, 631 | "schemaVersion": 38, 632 | "tags": [], 633 | "templating": { 634 | "list": [] 635 | }, 636 | "time": { 637 | "from": "2024-12-01T15:57:22.799Z", 638 | "to": "2024-12-01T15:58:36.816Z" 639 | }, 640 | "timepicker": {}, 641 | "timezone": "", 642 | "title": "AI Agent", 643 | "uid": "f905502c-8cab-4299-aeef-e00b0bfff106", 644 | "version": 1, 645 | "weekStart": "" 646 | } -------------------------------------------------------------------------------- /scripts/grafana/provisioning/dashboards/demo.yml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | providers: 3 | - allowUiUpdates: true 4 | disableDeletion: false 5 | name: demo 6 | options: 7 | foldersFromFilesStructure: true 8 | path: /etc/grafana/demo-dashboards 9 | updateIntervalSeconds: 10 10 | -------------------------------------------------------------------------------- /scripts/grafana/provisioning/datasources/demo.yml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | datasources: 3 | - access: proxy 4 | editable: false 5 | isDefault: true 6 | name: Prometheus 7 | type: prometheus 8 | url: http://prometheus:9090 9 | version: 1 10 | -------------------------------------------------------------------------------- /scripts/opentelemetry/config.yaml: -------------------------------------------------------------------------------- 1 | receivers: 2 | otlp: 3 | protocols: 4 | grpc: 5 | endpoint: 0.0.0.0:4317 6 | http: 7 | endpoint: 0.0.0.0:4318 8 | 9 | exporters: 10 | prometheus: 11 | endpoint: "0.0.0.0:8889" 12 | otlp/jaeger: 13 | endpoint: jaeger:4317 14 | tls: 15 | insecure: true 16 | 17 | connectors: 18 | spanmetrics: 19 | 20 | processors: 21 | batch: 22 | memory_limiter: 23 | check_interval: 5s 24 | limit_percentage: 65 25 | spike_limit_percentage: 60 26 | 27 | extensions: 28 | health_check: 29 | pprof: 30 | endpoint: :1888 31 | zpages: 32 | endpoint: :55679 33 | 34 | service: 35 | extensions: [ pprof, zpages, health_check ] 36 | pipelines: 37 | traces: 38 | receivers: [ otlp ] 39 | processors: 40 | - memory_limiter 41 | - batch 42 | exporters: 43 | - otlp/jaeger 44 | - spanmetrics 45 | 46 | metrics: 47 | receivers: [ otlp ] 48 | processors: 49 | - memory_limiter 50 | - batch 51 | exporters: 52 | - prometheus 53 | 54 | metrics/spanmetrics: 55 | receivers: [spanmetrics] 56 | exporters: [prometheus] 57 | -------------------------------------------------------------------------------- /scripts/prometheus/prometheus.yml: -------------------------------------------------------------------------------- 1 | scrape_configs: 2 | - job_name: otel_collector_exporter 3 | scrape_interval: 3s 4 | static_configs: 5 | - targets: [ 'otel_collector:8889' ] # application metrics collected by opentelemetry 6 | - targets: [ 'otel_collector:8888' ] # opentelemetry collector metrics 7 | -------------------------------------------------------------------------------- /ui/.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | vars.yml -------------------------------------------------------------------------------- /ui/Dockerfile: -------------------------------------------------------------------------------- 1 | # app/Dockerfile 2 | 3 | FROM python:3.9-slim 4 | 5 | WORKDIR /app 6 | 7 | RUN apt-get update && apt-get install -y \ 8 | build-essential \ 9 | curl \ 10 | software-properties-common \ 11 | git \ 12 | && rm -rf /var/lib/apt/lists/* 13 | 14 | COPY . . 15 | 16 | RUN pip3 install -r requirements.txt 17 | 18 | EXPOSE 8080 19 | 20 | HEALTHCHECK CMD curl --fail http://localhost:8080/_stcore/health 21 | 22 | ENTRYPOINT ["streamlit", "run", "server.py", "--server.port=8080", "--server.address=0.0.0.0"] -------------------------------------------------------------------------------- /ui/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | gcloud run deploy course-agent-ui --env-vars-file=vars.yml --allow-unauthenticated --source . --region us-central1 4 | -------------------------------------------------------------------------------- /ui/requirements.txt: -------------------------------------------------------------------------------- 1 | streamlit 2 | google-cloud-aiplatform -------------------------------------------------------------------------------- /ui/server.py: -------------------------------------------------------------------------------- 1 | import streamlit as st 2 | import uuid 3 | import vertexai 4 | import os 5 | from vertexai.preview import reasoning_engines 6 | 7 | PROJECT_ID = os.getenv("UI_PROJECT_ID") 8 | LOCATION = os.getenv("UI_LOCATION") 9 | STAGING_BUCKET = os.getenv("UI_STAGING_BUCKET") 10 | REASONING_ENGINE_PATH = os.getenv("UI_REASONING_ENGINE_PATH") 11 | 12 | vertexai.init(project=PROJECT_ID, location=LOCATION, staging_bucket=STAGING_BUCKET) 13 | remote_agent = reasoning_engines.ReasoningEngine(REASONING_ENGINE_PATH) 14 | 15 | st.title("Course Agent") 16 | 17 | def get_response(input: str): 18 | response = remote_agent.query( 19 | input=input, 20 | session_id=st.session_state.session_id, 21 | ) 22 | return response["output"] 23 | 24 | if "session_id" not in st.session_state: 25 | session_id = uuid.uuid4() 26 | st.session_state.session_id = str(session_id) 27 | 28 | if "messages" not in st.session_state: 29 | st.session_state.messages = [] 30 | 31 | # Display chat messages from history on app rerun 32 | for message in st.session_state.messages: 33 | with st.chat_message(message["role"]): 34 | st.markdown(message["content"]) 35 | 36 | # Accept user input 37 | if prompt := st.chat_input("What is up?"): 38 | st.session_state.messages.append({"role": "user", "content": prompt}) 39 | with st.chat_message("user"): 40 | st.markdown(prompt) 41 | 42 | response = get_response(prompt) 43 | with st.chat_message("assistant"): 44 | st.markdown(response) 45 | st.session_state.messages.append({"role": "assistant", "content": response}) -------------------------------------------------------------------------------- /ui/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # pip install -r requirements.txt 4 | 5 | export UI_PROJECT_ID="imrenagi-devfest-2024" 6 | export UI_LOCATION="us-central1" 7 | export UI_STAGING_BUCKET="gs://devfest24-demo-bucket" 8 | export UI_REASONING_ENGINE_PATH="projects/908311267620/locations/us-central1/reasoningEngines/2387523529017917440" 9 | 10 | streamlit run server.py --server.port=8080 --server.address=0.0.0.0 -------------------------------------------------------------------------------- /voice-agent/client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "io" 8 | "net/http" 9 | "time" 10 | ) 11 | 12 | type Course struct { 13 | Name string `json:"name"` 14 | DisplayName string `json:"display_name"` 15 | Description string `json:"description"` 16 | Price float64 `json:"price"` 17 | Currency string `json:"currency"` 18 | } 19 | 20 | type Order struct { 21 | ID string `json:"id"` 22 | Course string `json:"course"` 23 | Price float64 `json:"price"` 24 | Currency string `json:"currency"` 25 | UserEmail string `json:"user_email"` 26 | UserName string `json:"user_name"` 27 | Status string `json:"status"` 28 | CreatedAt time.Time `json:"created_at"` 29 | PaidAt time.Time `json:"paid_at"` 30 | } 31 | 32 | func ListCourse(ctx context.Context) ([]Course, error) { 33 | req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://localhost:8080/courses", nil) 34 | if err != nil { 35 | return nil, err 36 | } 37 | resp, err := http.DefaultClient.Do(req) 38 | if err != nil { 39 | return nil, err 40 | } 41 | defer resp.Body.Close() 42 | body, err := io.ReadAll(resp.Body) 43 | if err != nil { 44 | return nil, err 45 | } 46 | var courses []Course 47 | err = json.Unmarshal(body, &courses) 48 | if err != nil { 49 | return nil, err 50 | } 51 | return courses, nil 52 | } 53 | 54 | func GetCourse(ctx context.Context, course string) (*Course, error) { 55 | req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://localhost:8080/courses/"+course, nil) 56 | if err != nil { 57 | return nil, err 58 | } 59 | resp, err := http.DefaultClient.Do(req) 60 | if err != nil { 61 | return nil, err 62 | } 63 | defer resp.Body.Close() 64 | body, err := io.ReadAll(resp.Body) 65 | if err != nil { 66 | return nil, err 67 | } 68 | var courses Course 69 | err = json.Unmarshal(body, &courses) 70 | if err != nil { 71 | return nil, err 72 | } 73 | return &courses, nil 74 | } 75 | 76 | func CreateOrder(ctx context.Context, course, userName, userEmail string) (*Order, error) { 77 | type payload struct { 78 | Course string `json:"course"` 79 | UserName string `json:"user_name"` 80 | UserEmail string `json:"user_email"` 81 | } 82 | p := payload{ 83 | Course: course, 84 | UserName: userName, 85 | UserEmail: userEmail, 86 | } 87 | pb, err := json.Marshal(p) 88 | if err != nil { 89 | return nil, err 90 | } 91 | 92 | req, err := http.NewRequestWithContext(ctx, http.MethodPost, "http://localhost:8080/orders", bytes.NewReader(pb)) 93 | if err != nil { 94 | return nil, err 95 | } 96 | req.Header.Set("Content-Type", "application/json") 97 | resp, err := http.DefaultClient.Do(req) 98 | if err != nil { 99 | return nil, err 100 | } 101 | defer resp.Body.Close() 102 | body, err := io.ReadAll(resp.Body) 103 | if err != nil { 104 | return nil, err 105 | } 106 | 107 | var order Order 108 | err = json.Unmarshal(body, &order) 109 | if err != nil { 110 | return nil, err 111 | } 112 | return &order, nil 113 | } 114 | 115 | func GetOrder(ctx context.Context, orderNumber string) (*Order, error) { 116 | req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://localhost:8080/orders/"+orderNumber, nil) 117 | if err != nil { 118 | return nil, err 119 | } 120 | resp, err := http.DefaultClient.Do(req) 121 | if err != nil { 122 | return nil, err 123 | } 124 | defer resp.Body.Close() 125 | body, err := io.ReadAll(resp.Body) 126 | if err != nil { 127 | return nil, err 128 | } 129 | var order Order 130 | err = json.Unmarshal(body, &order) 131 | if err != nil { 132 | return nil, err 133 | } 134 | return &order, nil 135 | } 136 | -------------------------------------------------------------------------------- /voice-agent/courses/client.go: -------------------------------------------------------------------------------- 1 | package courses 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "io" 8 | "net/http" 9 | "time" 10 | ) 11 | 12 | type Course struct { 13 | Name string `json:"name"` 14 | DisplayName string `json:"display_name"` 15 | Description string `json:"description"` 16 | Price float64 `json:"price"` 17 | Currency string `json:"currency"` 18 | } 19 | 20 | type Order struct { 21 | ID string `json:"id"` 22 | Course string `json:"course"` 23 | Price float64 `json:"price"` 24 | Currency string `json:"currency"` 25 | UserEmail string `json:"user_email"` 26 | UserName string `json:"user_name"` 27 | Status string `json:"status"` 28 | CreatedAt time.Time `json:"created_at"` 29 | PaidAt time.Time `json:"paid_at"` 30 | } 31 | 32 | func ListCourse(ctx context.Context) ([]Course, error) { 33 | req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://localhost:8080/courses", nil) 34 | if err != nil { 35 | return nil, err 36 | } 37 | resp, err := http.DefaultClient.Do(req) 38 | if err != nil { 39 | return nil, err 40 | } 41 | defer resp.Body.Close() 42 | body, err := io.ReadAll(resp.Body) 43 | if err != nil { 44 | return nil, err 45 | } 46 | var courses []Course 47 | err = json.Unmarshal(body, &courses) 48 | if err != nil { 49 | return nil, err 50 | } 51 | return courses, nil 52 | } 53 | 54 | func GetCourse(ctx context.Context, course string) (*Course, error) { 55 | req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://localhost:8080/courses/"+course, nil) 56 | if err != nil { 57 | return nil, err 58 | } 59 | resp, err := http.DefaultClient.Do(req) 60 | if err != nil { 61 | return nil, err 62 | } 63 | defer resp.Body.Close() 64 | body, err := io.ReadAll(resp.Body) 65 | if err != nil { 66 | return nil, err 67 | } 68 | var courses Course 69 | err = json.Unmarshal(body, &courses) 70 | if err != nil { 71 | return nil, err 72 | } 73 | return &courses, nil 74 | } 75 | 76 | func CreateOrder(ctx context.Context, course, userName, userEmail string) (*Order, error) { 77 | type payload struct { 78 | Course string `json:"course"` 79 | UserName string `json:"user_name"` 80 | UserEmail string `json:"user_email"` 81 | } 82 | p := payload{ 83 | Course: course, 84 | UserName: userName, 85 | UserEmail: userEmail, 86 | } 87 | pb, err := json.Marshal(p) 88 | if err != nil { 89 | return nil, err 90 | } 91 | 92 | req, err := http.NewRequestWithContext(ctx, http.MethodPost, "http://localhost:8080/orders", bytes.NewReader(pb)) 93 | if err != nil { 94 | return nil, err 95 | } 96 | req.Header.Set("Content-Type", "application/json") 97 | resp, err := http.DefaultClient.Do(req) 98 | if err != nil { 99 | return nil, err 100 | } 101 | defer resp.Body.Close() 102 | body, err := io.ReadAll(resp.Body) 103 | if err != nil { 104 | return nil, err 105 | } 106 | 107 | var order Order 108 | err = json.Unmarshal(body, &order) 109 | if err != nil { 110 | return nil, err 111 | } 112 | return &order, nil 113 | } 114 | 115 | func GetOrder(ctx context.Context, orderNumber string) (*Order, error) { 116 | req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://localhost:8080/orders/"+orderNumber, nil) 117 | if err != nil { 118 | return nil, err 119 | } 120 | resp, err := http.DefaultClient.Do(req) 121 | if err != nil { 122 | return nil, err 123 | } 124 | defer resp.Body.Close() 125 | body, err := io.ReadAll(resp.Body) 126 | if err != nil { 127 | return nil, err 128 | } 129 | var order Order 130 | err = json.Unmarshal(body, &order) 131 | if err != nil { 132 | return nil, err 133 | } 134 | return &order, nil 135 | } 136 | -------------------------------------------------------------------------------- /voice-agent/courses/tools.go: -------------------------------------------------------------------------------- 1 | package courses 2 | 3 | import ( 4 | "google.golang.org/genai" 5 | ) 6 | 7 | var SystemPrompt = `You are a bot assistant that sells online course about software security. You only use information provided from datastore or tools. You can provide the information that is relevant to the user's question or the summary of the content. If they ask about the content, you can give them more detail about the content. If the user seems interested, you may suggest the user to enroll in the course.` 8 | 9 | var Tools = []*genai.Tool{ 10 | { 11 | FunctionDeclarations: []*genai.FunctionDeclaration{ 12 | { 13 | Name: "search_course_content", 14 | Description: "Explain about software security course materials.", 15 | Parameters: &genai.Schema{ 16 | Type: genai.TypeObject, 17 | Properties: map[string]*genai.Schema{ 18 | "query": { 19 | Type: genai.TypeString, 20 | Description: "search query to search course content.", 21 | }, 22 | }, 23 | Required: []string{"query"}, 24 | }, 25 | }, 26 | { 27 | Name: "list_courses", 28 | Description: "List all available courses sold on the platform.", 29 | }, 30 | { 31 | Name: "get_course", 32 | Description: "Get course details by course name. course name is the unique identifier of the course. it typically contains the course title with dashes. This function can be used to get course details such as course price, etc.", 33 | Parameters: &genai.Schema{ 34 | Type: genai.TypeObject, 35 | Properties: map[string]*genai.Schema{ 36 | "course": { 37 | Type: genai.TypeString, 38 | Description: "name of the course. this is the unique identifier of the course. it typically contains the course title with dashes, all in lowercase.", 39 | }, 40 | }, 41 | Required: []string{"course"}, 42 | }, 43 | }, 44 | { 45 | Name: "create_order", 46 | Description: "Create order for a course. This function can be used to create an order for a course. When this function returns successfully, it will return payment url to user to make payment.", 47 | Parameters: &genai.Schema{ 48 | Type: genai.TypeObject, 49 | Properties: map[string]*genai.Schema{ 50 | "course": { 51 | Type: genai.TypeString, 52 | Description: "name of the course. this is the unique identifier of the course. it typically contains the course title with dashes, all in lowercase.", 53 | }, 54 | "user_name": { 55 | Type: genai.TypeString, 56 | Description: "name of the user who is purchasing the course .", 57 | }, 58 | "user_email": { 59 | Type: genai.TypeString, 60 | Description: "email of the user who is purchasing the course .", 61 | }, 62 | }, 63 | Required: []string{"course", "user_name", "user_email"}, 64 | }, 65 | }, 66 | { 67 | Name: "get_order", 68 | Description: "Get order by using order number. This function can be used to get order details such as payment status to check whether the order has been paid or not. If user already paid the course, say thanks", 69 | Parameters: &genai.Schema{ 70 | Type: genai.TypeObject, 71 | Properties: map[string]*genai.Schema{ 72 | "order_number": { 73 | Type: genai.TypeString, 74 | Description: "order number identifier. this is a unique identifier in uuid format.", 75 | }, 76 | }, 77 | Required: []string{"order_number"}, 78 | }, 79 | }, 80 | }, 81 | }, 82 | } 83 | -------------------------------------------------------------------------------- /voice-agent/go.mod: -------------------------------------------------------------------------------- 1 | module voice-agent 2 | 3 | go 1.23.2 4 | 5 | require ( 6 | github.com/Masterminds/squirrel v1.5.4 7 | github.com/google/generative-ai-go v0.19.0 8 | github.com/gorilla/mux v1.8.1 9 | github.com/gorilla/websocket v1.5.3 10 | github.com/jmoiron/sqlx v1.4.0 11 | github.com/lib/pq v1.10.9 12 | github.com/rs/zerolog v1.33.0 13 | google.golang.org/api v0.220.0 14 | google.golang.org/genai v0.2.0 15 | ) 16 | 17 | require ( 18 | cloud.google.com/go v0.116.0 // indirect 19 | cloud.google.com/go/ai v0.10.0 // indirect 20 | cloud.google.com/go/auth v0.14.1 // indirect 21 | cloud.google.com/go/auth/oauth2adapt v0.2.7 // indirect 22 | cloud.google.com/go/compute/metadata v0.6.0 // indirect 23 | cloud.google.com/go/longrunning v0.6.2 // indirect 24 | github.com/felixge/httpsnoop v1.0.4 // indirect 25 | github.com/go-logr/logr v1.4.2 // indirect 26 | github.com/go-logr/stdr v1.2.2 // indirect 27 | github.com/google/go-cmp v0.6.0 // indirect 28 | github.com/google/s2a-go v0.1.9 // indirect 29 | github.com/google/uuid v1.6.0 // indirect 30 | github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect 31 | github.com/googleapis/gax-go/v2 v2.14.1 // indirect 32 | github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect 33 | github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect 34 | github.com/mattn/go-colorable v0.1.13 // indirect 35 | github.com/mattn/go-isatty v0.0.19 // indirect 36 | github.com/pgvector/pgvector-go v0.2.3 // indirect 37 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 38 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 // indirect 39 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect 40 | go.opentelemetry.io/otel v1.34.0 // indirect 41 | go.opentelemetry.io/otel/metric v1.34.0 // indirect 42 | go.opentelemetry.io/otel/trace v1.34.0 // indirect 43 | golang.org/x/crypto v0.32.0 // indirect 44 | golang.org/x/net v0.34.0 // indirect 45 | golang.org/x/oauth2 v0.25.0 // indirect 46 | golang.org/x/sync v0.10.0 // indirect 47 | golang.org/x/sys v0.29.0 // indirect 48 | golang.org/x/text v0.21.0 // indirect 49 | golang.org/x/time v0.9.0 // indirect 50 | google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect 51 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250127172529-29210b9bc287 // indirect 52 | google.golang.org/grpc v1.70.0 // indirect 53 | google.golang.org/protobuf v1.36.4 // indirect 54 | ) 55 | -------------------------------------------------------------------------------- /voice-agent/go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE= 2 | cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U= 3 | cloud.google.com/go/ai v0.10.0 h1:hwj6CI6sMKubXodoJJGTy/c2T1RbbLGM6TL3QoAvzU8= 4 | cloud.google.com/go/ai v0.10.0/go.mod h1:kvnt2KeHqX8+41PVeMRBETDyQAp/RFvBWGdx/aGjNMo= 5 | cloud.google.com/go/auth v0.14.1 h1:AwoJbzUdxA/whv1qj3TLKwh3XX5sikny2fc40wUl+h0= 6 | cloud.google.com/go/auth v0.14.1/go.mod h1:4JHUxlGXisL0AW8kXPtUF6ztuOksyfUQNFjfsOCXkPM= 7 | cloud.google.com/go/auth/oauth2adapt v0.2.7 h1:/Lc7xODdqcEw8IrZ9SvwnlLX6j9FHQM74z6cBk9Rw6M= 8 | cloud.google.com/go/auth/oauth2adapt v0.2.7/go.mod h1:NTbTTzfvPl1Y3V1nPpOgl2w6d/FjO7NNUQaWSox6ZMc= 9 | cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= 10 | cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= 11 | cloud.google.com/go/longrunning v0.6.2 h1:xjDfh1pQcWPEvnfjZmwjKQEcHnpz6lHjfy7Fo0MK+hc= 12 | cloud.google.com/go/longrunning v0.6.2/go.mod h1:k/vIs83RN4bE3YCswdXC5PFfWVILjm3hpEUlSko4PiI= 13 | filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= 14 | filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= 15 | github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= 16 | github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= 17 | github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 18 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 19 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 20 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 21 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 22 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 23 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 24 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 25 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 26 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 27 | github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= 28 | github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= 29 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 30 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 31 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 32 | github.com/google/generative-ai-go v0.19.0 h1:R71szggh8wHMCUlEMsW2A/3T+5LdEIkiaHSYgSpUgdg= 33 | github.com/google/generative-ai-go v0.19.0/go.mod h1:JYolL13VG7j79kM5BtHz4qwONHkeJQzOCkKXnpqtS/E= 34 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 35 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 36 | github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= 37 | github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= 38 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 39 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 40 | github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw= 41 | github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= 42 | github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= 43 | github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= 44 | github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= 45 | github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= 46 | github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= 47 | github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 48 | github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= 49 | github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= 50 | github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= 51 | github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= 52 | github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= 53 | github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= 54 | github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= 55 | github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 56 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 57 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 58 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 59 | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= 60 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 61 | github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= 62 | github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 63 | github.com/pgvector/pgvector-go v0.2.3 h1:/vv4mmSAtkT/XHCwkPexNiI1SNmrwccUqxPYr9WzIek= 64 | github.com/pgvector/pgvector-go v0.2.3/go.mod h1:u5sg3z9bnqVEdpe1pkTij8/rFhTaMCMNyQagPDLK8gQ= 65 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 66 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 67 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 68 | github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= 69 | github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= 70 | github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= 71 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 72 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 73 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 74 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 75 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 76 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 h1:PS8wXpbyaDJQ2VDHHncMe9Vct0Zn1fEjpsjrLxGJoSc= 77 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0/go.mod h1:HDBUsEjOuRC0EzKZ1bSaRGZWUBAzo+MhAcUUORSr4D0= 78 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU= 79 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= 80 | go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= 81 | go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= 82 | go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= 83 | go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= 84 | go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= 85 | go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= 86 | go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU= 87 | go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ= 88 | go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= 89 | go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= 90 | golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= 91 | golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= 92 | golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= 93 | golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= 94 | golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70= 95 | golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= 96 | golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= 97 | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 98 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 99 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 100 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 101 | golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= 102 | golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 103 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 104 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 105 | golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= 106 | golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 107 | google.golang.org/api v0.220.0 h1:3oMI4gdBgB72WFVwE1nerDD8W3HUOS4kypK6rRLbGns= 108 | google.golang.org/api v0.220.0/go.mod h1:26ZAlY6aN/8WgpCzjPNy18QpYaz7Zgg1h0qe1GkZEmY= 109 | google.golang.org/genai v0.2.0 h1:lWKoc2bru2VW9zBIgmBXQTKdT1oQ44W5Gfk950CkTNc= 110 | google.golang.org/genai v0.2.0/go.mod h1:yPyKKBezIg2rqZziLhHQ5CD62HWr7sLDLc2PDzdrNVs= 111 | google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q= 112 | google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08= 113 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250127172529-29210b9bc287 h1:J1H9f+LEdWAfHcez/4cvaVBox7cOYT+IU6rgqj5x++8= 114 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250127172529-29210b9bc287/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk= 115 | google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= 116 | google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= 117 | google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= 118 | google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 119 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 120 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 121 | -------------------------------------------------------------------------------- /voice-agent/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Talking Tom 5 | 6 | 71 | 72 | 73 |

Live Audio Input

74 | 75 | 76 |
77 |
78 |
79 |
80 |
81 | 82 | 112 | 113 | 345 | -------------------------------------------------------------------------------- /voice-agent/interviews/prompt.go: -------------------------------------------------------------------------------- 1 | package interviews 2 | 3 | var FriendlyPrompt = "You are a nice and friendly chatbot. Wait for a moment before you really respond to the user." 4 | 5 | var SystemPrompt = ` 6 | You are an AI Interviewer named "Eva." Your primary goal is to conduct effective and engaging interviews with candidates for a variety of roles. You must tailor your questions and demeanor to the specific role and the candidate's experience level. Your secondary goal is to evaluate the candidate's fitness based on the role's requirements. You should also be friendly, professional, and strive to create a positive interview experience for the candidate. 7 | 8 | **Here are your guidelines:** 9 | 10 | **1. Role Information (Crucial!):** You are given the job description, required skills, and company information. You *must* use this information to guide your questioning. This is the job description for the role the candidate is applying for: 11 | 12 | The GoTo Engineering Campus Hiring offers a full-time opportunity for individuals eager to pursue and craft impactful code. As part of the program participants will undergo a comprehensive Engineering Bootcamp, consisting of a series of intensive and accelerated learning programs specifically designed for junior engineers at Gojek and GoTo Financial. This thorough boot camp serves as an introduction to the engineering culture and principles crucial for our new hires to develop into proficient, world-class software engineers. 13 | 14 | What you will do: 15 | Design and develop highly scalable, reliable and fault-tolerant systems to translate business requirements into scalable and extensible design 16 | Coordinate with cross-functional teams (Mobile, DevOps, Data, UX, QA, etc.) on planning and execution 17 | Continuously improve code quality, product execution, and customer delight 18 | Communicate, collaborate, and work effectively across distributed teams and stakeholders in a global environment 19 | Building and managing fully automated build/test/deployment environments 20 | An innate desire to deliver and a strong sense of accountability for your work 21 | 22 | What you will need: 23 | Bachelor's degree, recently graduated or at least graduated 1 year ago. 24 | Have a clear, proven track record of building working software outside of your academic 25 | Passionate about learning new things and solving challenging problems 26 | You understand the right coding practices 27 | You write code because you like to and never stop wanting to get better at it 28 | A strong sense of ownership and passion for crafting delightful customer experiences 29 | Desire to be part of a team that delivers impactful results every day 30 | High Learning Agility 31 | 32 | **2. Interview Structure:** 33 | * **Introduction:** 34 | * Greet the candidate warmly and by name. 35 | * Introduce yourself and your role and ask the candidate to introduce themselves. 36 | * **Background & Experience:** 37 | * Explore the candidate's resume and work history. 38 | * Ask clarifying questions about their roles, responsibilities, and accomplishments. 39 | * Focus on experiences relevant to the target role, as detailed in the Role Briefing. 40 | * **Skills & Competencies:** 41 | * Assess the candidate's technical and soft skills. 42 | * Use behavioral questions (STAR method prompts) to understand how they've applied these skills in the past. 43 | * Examples: "Tell me about a time you faced a challenging problem at work. How did you approach it?", "Describe a situation where you had to work effectively with a difficult teammate.", "Give me an example of when you had to learn something new quickly." 44 | * **Culture Fit & Motivation:** 45 | * Gauge the candidate's alignment with the company's values and culture. 46 | * Understand their motivations for applying to the role. 47 | * Ask questions like: "What are you looking for in your next role?", "What are your career goals?", "What interests you about our company?". 48 | * **Candidate Questions:** 49 | * Allow the candidate to ask questions about the role, the team, or the company. 50 | * Provide thoughtful and informative answers. 51 | * **Wrap-up:** 52 | * Thank the candidate for their time. 53 | * Explain the next steps in the hiring process. 54 | * Provide a realistic timeline for when they can expect to hear back. 55 | 56 | **3. Questioning Techniques:** 57 | * **Behavioral Questions:** Use the STAR method (Situation, Task, Action, Result) to elicit detailed responses. Prompt the candidate to provide specific examples. 58 | * **Open-Ended Questions:** Encourage the candidate to elaborate and provide more context. 59 | * **Probing Questions:** Follow up on interesting or unclear points to gain a deeper understanding. 60 | * **Hypothetical Questions (Use sparingly):** Present hypothetical scenarios to assess problem-solving skills and decision-making. 61 | * **Avoid Leading Questions:** Frame questions neutrally to avoid influencing the candidate's response. 62 | 63 | **4. Tone & Style:** 64 | * Be professional, friendly, and approachable. 65 | * Use a conversational tone. 66 | * Actively listen to the candidate's responses. 67 | * Show empathy and understanding. 68 | * Avoid jargon or technical terms that the candidate may not be familiar with. 69 | * Adapt your communication style to match the candidate's personality. 70 | 71 | **5. Evaluation & Feedback:** 72 | * Based on the role brief, Evaluate the candidate's answers based on required skills, experience, and cultural fit. 73 | * Note down key information about the candidate's strengths and weaknesses. 74 | 75 | **6. Candidate Information:** 76 | 77 | ` 78 | -------------------------------------------------------------------------------- /voice-agent/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "os/signal" 7 | "syscall" 8 | 9 | gogenai "github.com/google/generative-ai-go/genai" 10 | "github.com/rs/zerolog/log" 11 | "google.golang.org/api/option" 12 | "google.golang.org/genai" 13 | 14 | _ "github.com/lib/pq" 15 | ) 16 | 17 | const ( 18 | project = "imrenagi-gemini-experiment" 19 | region = "us-central1" 20 | ) 21 | 22 | func main() { 23 | ctx := context.Background() 24 | ctx, cancel := context.WithCancel(ctx) 25 | ch := make(chan os.Signal, 1) 26 | signal.Notify(ch, os.Interrupt) 27 | signal.Notify(ch, syscall.SIGTERM) 28 | go func() { 29 | oscall := <-ch 30 | log.Warn().Msgf("system call:%+v", oscall) 31 | cancel() 32 | }() 33 | 34 | client, err := genai.NewClient(ctx, &genai.ClientConfig{ 35 | Project: project, 36 | Location: region, 37 | Backend: genai.BackendVertexAI, 38 | }) 39 | if err != nil { 40 | log.Fatal().Err(err).Msgf("create genai client error") 41 | } 42 | 43 | genAiClient, err := gogenai.NewClient(ctx, 44 | option.WithAPIKey(os.Getenv("GEMINI_API_KEY"))) 45 | if err != nil { 46 | log.Fatal().Err(err).Msgf("create golang genai client error") 47 | 48 | } 49 | defer genAiClient.Close() 50 | 51 | em := genAiClient.EmbeddingModel(embeddingModelName) 52 | 53 | db := NewSQLx() 54 | 55 | vectorStore := NewVectorStore(db) 56 | 57 | srv := &Server{ 58 | GenAIClient: client, 59 | EmbeddingModel: em, 60 | DB: db, 61 | VectorStore: vectorStore, 62 | } 63 | srv.Start(ctx) 64 | } 65 | -------------------------------------------------------------------------------- /voice-agent/pg.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/jmoiron/sqlx" 8 | ) 9 | 10 | func NewSQLx() *sqlx.DB { 11 | ds := fmt.Sprintf("user=%s password=%s host=%s port=%s dbname=%s sslmode=disable", 12 | os.Getenv("VOICE_AGENT_DB_USER"), 13 | os.Getenv("VOICE_AGENT_DB_PASSWORD"), 14 | os.Getenv("VOICE_AGENT_DB_HOST"), 15 | os.Getenv("VOICE_AGENT_DB_PORT"), 16 | os.Getenv("VOICE_AGENT_DB_NAME")) 17 | db, err := sqlx.Open("postgres", ds) 18 | if err != nil { 19 | panic(err) 20 | } 21 | return db 22 | } 23 | -------------------------------------------------------------------------------- /voice-agent/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "html/template" 7 | "time" 8 | 9 | "net/http" 10 | "voice-agent/courses" 11 | "voice-agent/interviews" 12 | 13 | _ "embed" 14 | 15 | gogenai "github.com/google/generative-ai-go/genai" 16 | "github.com/gorilla/mux" 17 | "github.com/gorilla/websocket" 18 | "github.com/jmoiron/sqlx" 19 | "github.com/rs/zerolog/log" 20 | "google.golang.org/genai" 21 | ) 22 | 23 | var upgrader = websocket.Upgrader{} 24 | 25 | const ( 26 | modelName = "gemini-2.0-flash-exp" 27 | embeddingModelName = "text-embedding-004" 28 | ) 29 | 30 | //go:embed index.html 31 | var homeTemplate string 32 | 33 | func (s Server) voiceChaHandler(model string, cfg *genai.LiveConnectConfig) http.HandlerFunc { 34 | return func(w http.ResponseWriter, r *http.Request) { 35 | c, err := upgrader.Upgrade(w, r, nil) 36 | if err != nil { 37 | log.Error().Err(err).Msg("upgrade websocket error") 38 | writeError(w, http.StatusInternalServerError, err) 39 | return 40 | } 41 | defer c.Close() 42 | 43 | session, err := s.GenAIClient.Live.Connect(model, cfg) 44 | if err != nil { 45 | log.Error().Err(err).Msg("unable to start live session") 46 | writeError(w, http.StatusInternalServerError, err) 47 | return 48 | } 49 | defer session.Close() 50 | 51 | errChan := make(chan error) 52 | doneChan := make(chan struct{}) 53 | 54 | // Get model's response 55 | go func() { 56 | for { 57 | message, err := session.Receive() 58 | if err != nil { 59 | log.Error().Err(err).Msg("receive error on live session response") 60 | errChan <- err 61 | return 62 | } 63 | 64 | var functionResponses []*genai.FunctionResponse 65 | if message.ToolCall != nil { 66 | functionCalls := message.ToolCall.FunctionCalls 67 | for _, fc := range functionCalls { 68 | log.Debug().Str("name", fc.Name). 69 | Str("id", fc.ID). 70 | Any("params", fc.Args). 71 | Msg("checking function call") 72 | fr, err := s.Dispatch(r.Context(), fc) 73 | if err != nil { 74 | log.Error().Err(err).Msg("dispatch error") 75 | errChan <- err 76 | return 77 | } 78 | functionResponses = append(functionResponses, fr) 79 | } 80 | log.Debug().Msg("sending tool response") 81 | err := session.Send(&genai.LiveClientMessage{ 82 | ToolResponse: &genai.LiveClientToolResponse{ 83 | FunctionResponses: functionResponses, 84 | }, 85 | }) 86 | if err != nil { 87 | log.Error().Err(err).Msg("send tool response error") 88 | errChan <- err 89 | return 90 | } 91 | log.Debug().Msg("tool response sent") 92 | } 93 | 94 | messageBytes, err := json.Marshal(message) 95 | if err != nil { 96 | log.Error().Err(err).Msg("marshal model response error") 97 | errChan <- err 98 | return 99 | } 100 | err = c.WriteMessage(websocket.TextMessage, messageBytes) 101 | if err != nil { 102 | if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseNormalClosure) { 103 | log.Error().Err(err).Msg("got unexpected websocket close error in write") 104 | errChan <- err 105 | } else { 106 | log.Debug().Err(err).Msg("websocket closed in write") 107 | doneChan <- struct{}{} 108 | } 109 | return 110 | } 111 | } 112 | }() 113 | 114 | go func() { 115 | for { 116 | _, message, err := c.ReadMessage() 117 | if err != nil { 118 | if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseNormalClosure) { 119 | log.Error().Err(err).Msg("got unexpected websocket close error in read") 120 | errChan <- err 121 | } else { 122 | log.Debug().Err(err).Msg("websocket closed in read") 123 | doneChan <- struct{}{} 124 | } 125 | return 126 | } 127 | 128 | // TODO currently the input is genai.LiveClientMessage, 129 | // we can create different contract with our client 130 | // and convert it to genai.LiveClientMessage later 131 | var sendMessage genai.LiveClientMessage 132 | if err := json.Unmarshal(message, &sendMessage); err != nil { 133 | log.Error().Err(err).Msg("unmarshal message error") 134 | errChan <- err 135 | return 136 | } 137 | 138 | if err := session.Send(&sendMessage); err != nil { 139 | log.Error().Err(err).Msg("send message to session error") 140 | errChan <- err 141 | return 142 | } 143 | } 144 | }() 145 | 146 | for { 147 | select { 148 | case <-doneChan: 149 | log.Debug().Msg("function done") 150 | return 151 | case err, ok := <-errChan: 152 | if !ok { 153 | log.Warn().Msg("error channel closed") 154 | return 155 | } else { 156 | writeError(w, http.StatusInternalServerError, err) 157 | return 158 | } 159 | } 160 | } 161 | } 162 | } 163 | 164 | func (s Server) CourseVoiceChaHandler() http.HandlerFunc { 165 | config := &genai.LiveConnectConfig{ 166 | GenerationConfig: &genai.GenerationConfig{ 167 | AudioTimestamp: true, 168 | }, 169 | ResponseModalities: []genai.Modality{ 170 | genai.ModalityAudio, 171 | genai.ModalityText, 172 | }, 173 | SpeechConfig: &genai.SpeechConfig{ 174 | VoiceConfig: &genai.VoiceConfig{ 175 | PrebuiltVoiceConfig: &genai.PrebuiltVoiceConfig{ 176 | VoiceName: "Kore", 177 | }, 178 | }, 179 | }, 180 | SystemInstruction: &genai.Content{ 181 | Parts: []*genai.Part{ 182 | { 183 | Text: courses.SystemPrompt, 184 | }, 185 | }, 186 | }, 187 | Tools: courses.Tools, 188 | } 189 | return s.voiceChaHandler(modelName, config) 190 | } 191 | 192 | func (s Server) InterviewerVoiceChaHandler() http.HandlerFunc { 193 | config := &genai.LiveConnectConfig{ 194 | GenerationConfig: &genai.GenerationConfig{ 195 | AudioTimestamp: true, 196 | }, 197 | ResponseModalities: []genai.Modality{ 198 | genai.ModalityAudio, 199 | genai.ModalityText, 200 | }, 201 | SpeechConfig: &genai.SpeechConfig{ 202 | VoiceConfig: &genai.VoiceConfig{ 203 | PrebuiltVoiceConfig: &genai.PrebuiltVoiceConfig{ 204 | VoiceName: "Kore", 205 | }, 206 | }, 207 | }, 208 | SystemInstruction: &genai.Content{ 209 | Parts: []*genai.Part{ 210 | { 211 | Text: interviews.SystemPrompt, 212 | }, 213 | }, 214 | }, 215 | } 216 | return s.voiceChaHandler(modelName, config) 217 | } 218 | 219 | func (s Server) CourseAgent() http.HandlerFunc { 220 | return s.voiceChatPage("/api/v1/courses/voice_sessions:start") 221 | } 222 | 223 | func (s Server) InterviewAgent() http.HandlerFunc { 224 | return s.voiceChatPage("/api/v1/interviewers/voice_sessions:start") 225 | } 226 | 227 | func (s Server) voiceChatPage(path string) http.HandlerFunc { 228 | return func(w http.ResponseWriter, r *http.Request) { 229 | tmpl, err := template.New("home").Parse(homeTemplate) 230 | if err != nil { 231 | http.Error(w, "Error loading template", http.StatusInternalServerError) 232 | return 233 | } 234 | 235 | err = tmpl.Execute(w, "ws://"+r.Host+path) 236 | if err != nil { 237 | http.Error(w, "Error executing template", http.StatusInternalServerError) 238 | return 239 | } 240 | } 241 | } 242 | 243 | type Server struct { 244 | GenAIClient *genai.Client 245 | EmbeddingModel *gogenai.EmbeddingModel 246 | DB *sqlx.DB 247 | VectorStore *VectorStore 248 | } 249 | 250 | func (s *Server) Start(ctx context.Context) { 251 | mux := mux.NewRouter() 252 | mux.Handle("/courses", s.CourseAgent()) 253 | mux.Handle("/interviews", s.InterviewAgent()) 254 | 255 | api := mux.PathPrefix("/api").Subrouter() 256 | 257 | coursesV1Router := api.PathPrefix("/v1/courses").Subrouter() 258 | coursesV1Router.Handle("/voice_sessions:start", s.CourseVoiceChaHandler()) 259 | 260 | interviewersV1Router := api.PathPrefix("/v1/interviewers").Subrouter() 261 | interviewersV1Router.Handle("/voice_sessions:start", s.InterviewerVoiceChaHandler()) 262 | 263 | server := &http.Server{ 264 | Addr: ":8000", 265 | Handler: mux, 266 | ReadTimeout: 15 * time.Minute, 267 | WriteTimeout: 15 * time.Minute, 268 | ReadHeaderTimeout: 10 * time.Second, 269 | IdleTimeout: 10 * time.Second, 270 | } 271 | go func() { 272 | if err := server.ListenAndServe(); err != nil { 273 | log.Fatal().Msgf("failed to start server: %v", err) 274 | } 275 | }() 276 | 277 | <-ctx.Done() 278 | 279 | gracefulShutdownPeriod := 5 * time.Second 280 | 281 | log.Warn().Msg("shutting down http server") 282 | shutdownCtx, cancel := context.WithTimeout(context.Background(), gracefulShutdownPeriod) 283 | defer cancel() 284 | if err := server.Shutdown(shutdownCtx); err != nil { 285 | log.Error().Err(err).Msg("failed to shutdown http server gracefully") 286 | } 287 | } 288 | 289 | type cError struct { 290 | Message string `json:"message"` 291 | } 292 | 293 | func writeError(w http.ResponseWriter, code int, err error) { 294 | w.WriteHeader(code) 295 | b, _ := json.Marshal(cError{Message: err.Error()}) 296 | w.Header().Set("Content-Type", "application/json") 297 | w.Write(b) 298 | } 299 | -------------------------------------------------------------------------------- /voice-agent/store.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | 6 | sq "github.com/Masterminds/squirrel" 7 | "github.com/jmoiron/sqlx" 8 | "github.com/pgvector/pgvector-go" 9 | "github.com/rs/zerolog/log" 10 | ) 11 | 12 | func NewVectorStore(db *sqlx.DB) *VectorStore { 13 | return &VectorStore{ 14 | db: db, 15 | dbCache: sq.NewStmtCache(db), 16 | } 17 | } 18 | 19 | type VectorStore struct { 20 | db *sqlx.DB 21 | dbCache *sq.StmtCache 22 | } 23 | 24 | func (s *VectorStore) QueryContent(ctx context.Context, query []float32, similarityThreshold float32) ([]string, error) { 25 | sb := sq.StatementBuilder.RunWith(s.dbCache) 26 | selectCourses := sb. 27 | // Select("langchain_id", "content", "embedding", "langchain_metadata"). 28 | Select("content", "1 - (c.embedding <=> $1) as distance"). 29 | From("course_content_embeddings c"). 30 | Where("1 - (c.embedding <=> $1) > $2", 31 | pgvector.NewVector(query), similarityThreshold). 32 | OrderBy("distance desc"). 33 | Limit(5). 34 | PlaceholderFormat(sq.Dollar) 35 | 36 | rows, err := selectCourses.QueryContext(ctx) 37 | if err != nil { 38 | log.Error().Err(err).Msg("query content error") 39 | return nil, err 40 | } 41 | 42 | var contents []string 43 | for rows.Next() { 44 | var content string 45 | var distance float32 46 | if err := rows.Scan(&content, &distance); err != nil { 47 | log.Error().Err(err).Msg("scan content error") 48 | return nil, err 49 | } 50 | // log.Debug().Str("content", content).Float32("distance", distance).Msg("content") 51 | contents = append(contents, content) 52 | } 53 | return contents, nil 54 | } 55 | -------------------------------------------------------------------------------- /voice-agent/tools.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | 8 | "voice-agent/courses" 9 | 10 | "github.com/rs/zerolog/log" 11 | 12 | gogenai "github.com/google/generative-ai-go/genai" 13 | "google.golang.org/genai" 14 | ) 15 | 16 | func (s Server) Dispatch(ctx context.Context, fc *genai.FunctionCall) (*genai.FunctionResponse, error) { 17 | switch fc.Name { 18 | case "list_courses": 19 | return s.ListCourses(ctx) 20 | case "get_course": 21 | return s.GetCourse(ctx, fc) 22 | case "create_order": 23 | return s.CreateOrder(ctx, fc) 24 | case "get_order": 25 | return s.GetOrder(ctx, fc) 26 | case "search_course_content": 27 | return s.SearchCourseContent(ctx, fc.Args["query"].(string)) 28 | default: 29 | return nil, fmt.Errorf("unknown function %s", fc.Name) 30 | } 31 | } 32 | 33 | func (s Server) GetOrder(ctx context.Context, fc *genai.FunctionCall) (*genai.FunctionResponse, error) { 34 | orderNumber, ok := fc.Args["order_number"].(string) 35 | if !ok { 36 | return nil, fmt.Errorf("missing order number") 37 | } 38 | o, err := courses.GetOrder(ctx, orderNumber) 39 | if err != nil { 40 | return nil, err 41 | } 42 | var om map[string]any 43 | b, _ := json.Marshal(o) 44 | err = json.Unmarshal(b, &om) 45 | if err != nil { 46 | return nil, err 47 | } 48 | log.Debug().Interface("order", om).Msg("checking order") 49 | fr := &genai.FunctionResponse{ 50 | Name: "get_order", 51 | Response: map[string]interface{}{ 52 | "order": om, 53 | }, 54 | } 55 | return fr, nil 56 | } 57 | 58 | func (s Server) CreateOrder(ctx context.Context, fc *genai.FunctionCall) (*genai.FunctionResponse, error) { 59 | course, ok := fc.Args["course"].(string) 60 | if !ok { 61 | return nil, fmt.Errorf("missing course") 62 | } 63 | userName, ok := fc.Args["user_name"].(string) 64 | if !ok { 65 | return nil, fmt.Errorf("missing user name") 66 | } 67 | userEmail, ok := fc.Args["user_email"].(string) 68 | if !ok { 69 | return nil, fmt.Errorf("missing user email") 70 | } 71 | o, err := courses.CreateOrder(ctx, course, userName, userEmail) 72 | if err != nil { 73 | return nil, err 74 | } 75 | 76 | var om map[string]any 77 | b, _ := json.Marshal(o) 78 | err = json.Unmarshal(b, &om) 79 | if err != nil { 80 | return nil, err 81 | } 82 | log.Debug().Interface("order", om).Msg("checking order") 83 | 84 | paymentUrl := fmt.Sprintf("http://localhost:8080/orders/%s/payment", o.ID) 85 | log.Info().Str("payment_url", paymentUrl).Msg("payment url") 86 | 87 | return &genai.FunctionResponse{ 88 | Name: "create_order", 89 | Response: map[string]interface{}{ 90 | "order": om, 91 | "payment_url": paymentUrl, 92 | }, 93 | }, nil 94 | } 95 | 96 | func (s Server) GetCourse(ctx context.Context, fc *genai.FunctionCall) (*genai.FunctionResponse, error) { 97 | course, ok := fc.Args["course"].(string) 98 | if !ok { 99 | return nil, fmt.Errorf("missing course") 100 | } 101 | 102 | c, err := courses.GetCourse(ctx, course) 103 | if err != nil { 104 | return nil, err 105 | } 106 | var cm map[string]any 107 | b, _ := json.Marshal(c) 108 | err = json.Unmarshal(b, &cm) 109 | if err != nil { 110 | return nil, err 111 | } 112 | log.Debug().Interface("course", cm).Msg("checking course") 113 | fr := &genai.FunctionResponse{ 114 | Name: "get_course", 115 | Response: map[string]interface{}{ 116 | "course": cm, 117 | }, 118 | } 119 | return fr, nil 120 | } 121 | 122 | func (s Server) ListCourses(ctx context.Context) (*genai.FunctionResponse, error) { 123 | courses, err := courses.ListCourse(ctx) 124 | if err != nil { 125 | return nil, err 126 | } 127 | var coursesM []map[string]interface{} 128 | b, _ := json.Marshal(courses) 129 | err = json.Unmarshal(b, &coursesM) 130 | if err != nil { 131 | return nil, err 132 | } 133 | log.Debug().Interface("courses", coursesM).Msg("checking courses") 134 | fr := &genai.FunctionResponse{ 135 | Name: "list_courses", 136 | Response: map[string]interface{}{ 137 | "courses": coursesM, 138 | }, 139 | } 140 | return fr, nil 141 | } 142 | 143 | func (s Server) SearchCourseContent(ctx context.Context, query string) (*genai.FunctionResponse, error) { 144 | resp, err := s.EmbeddingModel.EmbedContent(ctx, gogenai.Text(query)) 145 | if err != nil { 146 | return nil, err 147 | } 148 | data, err := s.VectorStore.QueryContent(ctx, resp.Embedding.Values, 0) 149 | if err != nil { 150 | return nil, err 151 | } 152 | fr := &genai.FunctionResponse{ 153 | Name: "search_course_content", 154 | Response: map[string]interface{}{ 155 | "contents": data, 156 | }, 157 | } 158 | return fr, nil 159 | } 160 | --------------------------------------------------------------------------------